From f44a797429de970029b37e9286eba499ef945f33 Mon Sep 17 00:00:00 2001 From: Jeremy Greenwood Date: Thu, 20 Feb 2025 11:26:03 -0500 Subject: [PATCH 01/11] Enable Swift 6 Mode (#38) --- Package.swift | 10 +++++----- README.md | 3 +++ .../OpenWeatherKit/Public/Celestial/MoonEvents.swift | 2 +- .../OpenWeatherKit/Public/Celestial/MoonPhase.swift | 2 +- .../OpenWeatherKit/Public/Celestial/SunEvents.swift | 2 +- .../Public/Characteristics/AlertSummary.swift | 2 +- .../Public/Characteristics/AlertUrgency.swift | 2 +- .../Public/Characteristics/Certainty.swift | 2 +- .../Public/Characteristics/Precipitation.swift | 2 +- .../Public/Characteristics/PressureTrend.swift | 2 +- .../Public/Characteristics/UVIndex.swift | 2 +- .../Public/Characteristics/WeatherCondition.swift | 2 +- .../Public/Characteristics/WeatherSeverity.swift | 2 +- .../OpenWeatherKit/Public/Characteristics/Wind.swift | 2 +- .../OpenWeatherKit/Public/Forecast/DayWeather.swift | 2 +- Sources/OpenWeatherKit/Public/Forecast/Forecast.swift | 2 +- .../OpenWeatherKit/Public/Forecast/HourWeather.swift | 2 +- .../OpenWeatherKit/Public/Forecast/MinuteWeather.swift | 2 +- .../OpenWeatherKit/Public/Forecast/WeatherAlert.swift | 2 +- .../Public/Forecast/WeatherAvailability.swift | 2 +- .../Public/Protocols/LocationProtocol.swift | 4 ++-- .../Public/Requests/CurrentWeather.swift | 2 +- .../Public/Requests/WeatherAttribution.swift | 2 +- .../Public/Requests/WeatherMetadata.swift | 2 +- .../OpenWeatherKit/Public/Requests/WeatherQuery.swift | 2 +- Sources/OpenWeatherKit/Public/Weather.swift | 2 +- Sources/OpenWeatherKit/Public/WeatherError.swift | 2 +- Sources/OpenWeatherKit/Public/WeatherService.swift | 4 ++-- 28 files changed, 36 insertions(+), 33 deletions(-) diff --git a/Package.swift b/Package.swift index 736217f..f9d5c82 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.5 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -10,7 +10,8 @@ let package = Package( .iOS(.v13), .watchOS(.v6), .tvOS(.v13), - .macOS(.v11) + .macOS(.v11), + .visionOS(.v1) ], products: [ .library( @@ -27,12 +28,11 @@ let package = Package( .testTarget( name: "OpenWeatherKitTests", dependencies: ["OpenWeatherKit"]), - ] + ], + swiftLanguageVersions: [.version("6"), .v5] ) #if os(Linux) package.dependencies.append(.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.19.0")) package.targets.first { $0.name == "OpenWeatherKit" }?.dependencies.append(.product(name: "AsyncHTTPClient", package: "async-http-client")) #endif - - diff --git a/README.md b/README.md index 4a47702..887117b 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,12 @@ is nearly identical to Apple's [WeatherKit](https://developer.apple.com/document ## 💻 Supported Platforms +Minimum Swift version of 5.9 + - iOS 13+ - watchOS 6+ - tvOS 13+ +- visionOS 1+ - macOS 11+ - Ubuntu 18.04+ diff --git a/Sources/OpenWeatherKit/Public/Celestial/MoonEvents.swift b/Sources/OpenWeatherKit/Public/Celestial/MoonEvents.swift index ea66d6c..437865a 100644 --- a/Sources/OpenWeatherKit/Public/Celestial/MoonEvents.swift +++ b/Sources/OpenWeatherKit/Public/Celestial/MoonEvents.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that represents lunar events. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct MoonEvents: Sendable { /// The moon phase. diff --git a/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift b/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift index 55b1409..917030e 100644 --- a/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift +++ b/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift @@ -8,7 +8,7 @@ import Foundation /// An enumeration that specifies the moon phase kind. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @frozen public enum MoonPhase : String, CustomStringConvertible, CaseIterable { enum CodingKeys: String, CodingKey { case new, waxingCrescent, firstQuarter, waxingGibbous, full, waningGibbous, waningCrescent diff --git a/Sources/OpenWeatherKit/Public/Celestial/SunEvents.swift b/Sources/OpenWeatherKit/Public/Celestial/SunEvents.swift index 1719e0c..80b50e7 100644 --- a/Sources/OpenWeatherKit/Public/Celestial/SunEvents.swift +++ b/Sources/OpenWeatherKit/Public/Celestial/SunEvents.swift @@ -8,7 +8,7 @@ import Foundation /// An enumeration that represents dates of solar events, including sunrise, sunset, dawn, and dusk. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct SunEvents: Sendable { /// The time of astronomical sunrise when the sun’s center is 18° below the horizon. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift b/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift index 0e49eb3..9d36a1b 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift @@ -8,7 +8,7 @@ import Foundation /// All information related to the weather alert -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct AlertSummary: Codable, Equatable, Sendable { public var name: String public var id: String diff --git a/Sources/OpenWeatherKit/Public/Characteristics/AlertUrgency.swift b/Sources/OpenWeatherKit/Public/Characteristics/AlertUrgency.swift index 67b5026..288e586 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/AlertUrgency.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/AlertUrgency.swift @@ -8,7 +8,7 @@ import Foundation /// The urgency of the weather alert -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum AlertUrgency: String, Codable, Equatable, Sendable { // Take responsive action immediately. case immediate diff --git a/Sources/OpenWeatherKit/Public/Characteristics/Certainty.swift b/Sources/OpenWeatherKit/Public/Characteristics/Certainty.swift index c0b5935..70b42cb 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/Certainty.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/Certainty.swift @@ -8,7 +8,7 @@ import Foundation /// The likelihood the weather alert event will happen -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum Certainty: String, Codable, Equatable, Sendable { // The event has already occurred or is ongoing. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift b/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift index 37b4df5..1661a15 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift @@ -8,7 +8,7 @@ import Foundation /// The form of precipitation. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum Precipitation : String, CaseIterable, CustomStringConvertible, Hashable, Sendable { /// No precipitation. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift b/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift index 47d1237..062e3ca 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift @@ -8,7 +8,7 @@ import Foundation /// The atmospheric pressure change over time. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum PressureTrend : String, CaseIterable, CustomStringConvertible, Hashable, Sendable { /// The pressure is rising. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift b/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift index 0736329..1c10414 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift @@ -8,7 +8,7 @@ import Foundation /// The expected intensity of ultraviolet radiation from the sun. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct UVIndex: Sendable { /// The UV Index value. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift b/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift index 442d10a..c993952 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift @@ -8,7 +8,7 @@ import Foundation /// A description of the current weather condition. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum WeatherCondition : String, CaseIterable, CustomStringConvertible, Hashable, Sendable { /// The kind of condition. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift b/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift index 1606846..5bc71ff 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift @@ -8,7 +8,7 @@ import Foundation /// A description of the severity of the severe weather event. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum WeatherSeverity : String, Codable, CaseIterable, CustomStringConvertible, Hashable, Sendable { /// Specifies "minimal" or no threat to life or property. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift b/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift index ca5dc02..785229c 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift @@ -8,7 +8,7 @@ import Foundation /// Contains wind data of speed, direction, and gust. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Wind: Sendable { /// General indicator of wind direction, often referred to as "due north", "due south", etc. diff --git a/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift b/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift index 52e694e..9f1e454 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that represents the weather conditions for the day. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct DayWeather: Sendable { /// The start date of the day weather. diff --git a/Sources/OpenWeatherKit/Public/Forecast/Forecast.swift b/Sources/OpenWeatherKit/Public/Forecast/Forecast.swift index d8443f4..b9559c4 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/Forecast.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/Forecast.swift @@ -8,7 +8,7 @@ import Foundation /// A forecast collection for minute, hourly, and daily forecasts. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Forecast : RandomAccessCollection, Codable, Equatable, Sendable where Element : Decodable, Element : Encodable, Element : Equatable, Element : Sendable { public init(forecast: [Element], metadata: WeatherMetadata) { self.forecast = forecast diff --git a/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift b/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift index de59043..373a248 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that represents the weather conditions for the hour. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct HourWeather: Sendable { /// The start date of the hour weather. diff --git a/Sources/OpenWeatherKit/Public/Forecast/MinuteWeather.swift b/Sources/OpenWeatherKit/Public/Forecast/MinuteWeather.swift index ccd9753..745a5d9 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/MinuteWeather.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/MinuteWeather.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that represents the next hour minute forecasts. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct MinuteWeather: Sendable { /// The start date of the minute weather. diff --git a/Sources/OpenWeatherKit/Public/Forecast/WeatherAlert.swift b/Sources/OpenWeatherKit/Public/Forecast/WeatherAlert.swift index 65b98bd..42bf854 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/WeatherAlert.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/WeatherAlert.swift @@ -8,7 +8,7 @@ import Foundation /// A weather alert issued for the requested location by a governmental authority. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct WeatherAlert: Sendable { /// The site for more details about the weather alert. Required link for attribution. public var detailsURL: URL diff --git a/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift b/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift index e30557f..fc05167 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that indicates the availability of data at the requested location. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct WeatherAvailability: Sendable { /// The minute forecast availability. diff --git a/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift b/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift index 69f273e..609ab20 100644 --- a/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift +++ b/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift @@ -16,7 +16,7 @@ extension CLLocation: LocationProtocol, @unchecked Sendable { #endif /// Defines the interface for a location -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public protocol LocationProtocol: Sendable { var latitude: Double { get } var longitude: Double { get } @@ -24,7 +24,7 @@ public protocol LocationProtocol: Sendable { init(latitude: Double, longitude: Double) } -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Location: LocationProtocol, Equatable, Codable, Sendable { public let latitude: Double public let longitude: Double diff --git a/Sources/OpenWeatherKit/Public/Requests/CurrentWeather.swift b/Sources/OpenWeatherKit/Public/Requests/CurrentWeather.swift index 21bb3f7..7444b00 100644 --- a/Sources/OpenWeatherKit/Public/Requests/CurrentWeather.swift +++ b/Sources/OpenWeatherKit/Public/Requests/CurrentWeather.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that describes the current conditions observed at a location. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct CurrentWeather: Sendable { public init( date: Date, diff --git a/Sources/OpenWeatherKit/Public/Requests/WeatherAttribution.swift b/Sources/OpenWeatherKit/Public/Requests/WeatherAttribution.swift index f9c4527..fc9a93d 100644 --- a/Sources/OpenWeatherKit/Public/Requests/WeatherAttribution.swift +++ b/Sources/OpenWeatherKit/Public/Requests/WeatherAttribution.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that defines the necessary information for attributing a weather data provider. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct WeatherAttribution { /// The weather data provider name. diff --git a/Sources/OpenWeatherKit/Public/Requests/WeatherMetadata.swift b/Sources/OpenWeatherKit/Public/Requests/WeatherMetadata.swift index 3f79ef0..8738022 100644 --- a/Sources/OpenWeatherKit/Public/Requests/WeatherMetadata.swift +++ b/Sources/OpenWeatherKit/Public/Requests/WeatherMetadata.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that provides additional weather information. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct WeatherMetadata: Sendable { /// The date of the weather data request. diff --git a/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift b/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift index fbde30c..d537b03 100644 --- a/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift +++ b/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates a generic weather dataset request. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct WeatherQuery { @usableFromInline let queryType: QueryType diff --git a/Sources/OpenWeatherKit/Public/Weather.swift b/Sources/OpenWeatherKit/Public/Weather.swift index c19b9a7..dde31b9 100644 --- a/Sources/OpenWeatherKit/Public/Weather.swift +++ b/Sources/OpenWeatherKit/Public/Weather.swift @@ -8,7 +8,7 @@ import Foundation /// A model representing the aggregate weather data the caller requests. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Weather: Sendable { /// The current weather forecast. diff --git a/Sources/OpenWeatherKit/Public/WeatherError.swift b/Sources/OpenWeatherKit/Public/WeatherError.swift index e045f4a..0f79dbc 100644 --- a/Sources/OpenWeatherKit/Public/WeatherError.swift +++ b/Sources/OpenWeatherKit/Public/WeatherError.swift @@ -8,7 +8,7 @@ import Foundation /// An error WeatherKit returns. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum WeatherError : LocalizedError, Equatable, Hashable { /// Could not find country code case countryCode diff --git a/Sources/OpenWeatherKit/Public/WeatherService.swift b/Sources/OpenWeatherKit/Public/WeatherService.swift index de3b210..bf213b1 100644 --- a/Sources/OpenWeatherKit/Public/WeatherService.swift +++ b/Sources/OpenWeatherKit/Public/WeatherService.swift @@ -15,11 +15,11 @@ import CoreLocation #endif /// Provides an interface for obtaining weather data. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) final public class WeatherService: Sendable { /// Establishes the configuration for weather requests. - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Configuration: Sendable { public enum Language: String, Sendable { From 4860dad71f376c1c820abca0ecb8ffe29a109fa4 Mon Sep 17 00:00:00 2001 From: Jeremy Greenwood Date: Thu, 23 Oct 2025 06:36:44 -0400 Subject: [PATCH 02/11] v2 api (#40) --- .devcontainer/devcontainer.json | 2 +- .github/workflows/swift-ubuntu.yml | 18 +- .../Extensions/APIForecastDaily+Map.swift | 51 +++- .../Extensions/APIForecastHourly+Map.swift | 14 +- .../Extensions/APIForecastNextHour+Map.swift | 25 +- .../Extensions/APIWeatherAlerts+Map.swift | 3 +- .../APIWeatherAvailability+Map.swift | 4 +- .../Internal/Extensions/Int+Utils.swift | 12 + .../Extensions/Measurement+Utils.swift | 18 ++ .../Extensions/WeatherQuery+QueryItems.swift | 3 +- .../OpenWeatherKit/Internal/Geocoder.swift | 30 ++- .../Internal/Models/APICurrentWeather.swift | 10 +- .../Internal/Models/APIForecastDaily.swift | 130 +++++++--- .../Internal/Models/APIForecastHourly.swift | 26 +- .../Internal/Models/APIForecastNextHour.swift | 32 ++- .../Internal/Models/APIMetadata.swift | 20 +- .../Models/APIPrecipitationAmountByType.swift | 62 +++++ .../Internal/Models/APIWeatherAlerts.swift | 89 +++---- .../Internal/Models/TextCaseCoding.swift | 83 +++++++ .../Internal/NetworkClient.swift | 15 +- Sources/OpenWeatherKit/Internal/Route.swift | 6 +- .../Public/Celestial/MoonPhase.swift | 67 ++---- .../Public/Characteristics/AlertSummary.swift | 5 +- .../CloudCoverByAltitude.swift | 35 +++ .../Characteristics/DayPartForecast.swift | 70 ++++++ .../Characteristics/Precipitation.swift | 24 +- .../PrecipitationAmountByType.swift | 35 +++ .../Characteristics/PressureTrend.swift | 16 +- .../Characteristics/SnowfallAmount.swift | 35 +++ .../Public/Characteristics/UVIndex.swift | 20 +- .../Characteristics/WeatherCondition.swift | 140 +++++------ .../Characteristics/WeatherSeverity.swift | 20 +- .../Public/Characteristics/Wind.swift | 102 ++++---- .../Public/Forecast/DayWeather.swift | 99 +++++++- .../Public/Forecast/HourWeather.swift | 10 +- .../OpenWeatherKit/Public/WeatherError.swift | 25 +- .../Public/WeatherService.swift | 226 ++++++++++++------ .../OpenWeatherKitTests.swift | 22 +- .../Utils/Geocoder+Mock.swift | 5 +- .../Utils/MockClient.swift | 8 +- .../OpenWeatherKitTests/Utils/MockData.swift | 12 +- 41 files changed, 1153 insertions(+), 476 deletions(-) create mode 100644 Sources/OpenWeatherKit/Internal/Extensions/Int+Utils.swift create mode 100644 Sources/OpenWeatherKit/Internal/Extensions/Measurement+Utils.swift create mode 100644 Sources/OpenWeatherKit/Internal/Models/APIPrecipitationAmountByType.swift create mode 100644 Sources/OpenWeatherKit/Internal/Models/TextCaseCoding.swift create mode 100644 Sources/OpenWeatherKit/Public/Characteristics/CloudCoverByAltitude.swift create mode 100644 Sources/OpenWeatherKit/Public/Characteristics/DayPartForecast.swift create mode 100644 Sources/OpenWeatherKit/Public/Characteristics/PrecipitationAmountByType.swift create mode 100644 Sources/OpenWeatherKit/Public/Characteristics/SnowfallAmount.swift diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c7e2437..1109f00 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -29,7 +29,7 @@ }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ - "sswg.swift-lang" + "swiftlang.swift-vscode" ] } }, diff --git a/.github/workflows/swift-ubuntu.yml b/.github/workflows/swift-ubuntu.yml index 7f48174..860c31f 100644 --- a/.github/workflows/swift-ubuntu.yml +++ b/.github/workflows/swift-ubuntu.yml @@ -3,7 +3,7 @@ name: Swift Ubuntu -on: +on: push: branches: - main @@ -25,11 +25,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup Swift + - name: Install Dependencies + run: sudo apt-get -y install libcurl4-openssl-dev pkg-config python3-lldb-13 + + - name: Install Swiftly + run: | + curl -O https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz && \ + tar zxf swiftly-$(uname -m).tar.gz && \ + ./swiftly init --skip-install --assume-yes + + . ~/.local/share/swiftly/env.sh + echo "PATH=$PATH" >> $GITHUB_ENV + + - name: Install Swift run: | - curl -L https://swiftlang.github.io/swiftly/swiftly-install.sh | bash -s -- --disable-confirmation swiftly install ${{ matrix.swift }} - swiftly use ${{ matrix.swift }} - name: Get Swift version run: swift --version diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIForecastDaily+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIForecastDaily+Map.swift index 362bfa3..610f5d7 100644 --- a/Sources/OpenWeatherKit/Internal/Extensions/APIForecastDaily+Map.swift +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIForecastDaily+Map.swift @@ -23,11 +23,14 @@ extension APIDay { condition: WeatherCondition(rawValue: conditionCode) ?? .undefined, symbolName: (WeatherCondition(rawValue: conditionCode) ?? .undefined).sfSymbol, highTemperature: Measurement(value: temperatureMax, unit: .celsius), + highTemperatureTime: temperatureMaxTime, lowTemperature: Measurement(value: temperatureMin, unit: .celsius), + lowTemperatureTime: temperatureMinTime, + maximumHumidity: humidityMax.percentage, + minimumHumidity: humidityMin.percentage, precipitation: Precipitation(rawValue: precipitationType) ?? .none, - precipitationChance: precipitationChance, - rainfallAmount: Measurement(value: precipitationAmount, unit: .millimeters), - snowfallAmount: Measurement(value: snowfallAmount, unit: .millimeters), + precipitationChance: precipitationChance.percentage, + precipitationAmountByType: precipitationAmountByType.precipitationAmountByType, sun: SunEvents( astronomicalDawn: sunriseAstronomical, nauticalDawn: sunriseNautical, @@ -46,10 +49,48 @@ extension APIDay { moonset: moonset ), uvIndex: UVIndex(value: maxUvIndex, category: .init(value: maxUvIndex)), + maximumVisibility: visibilityMax, + minimumVisibility: visibilityMin, wind: Wind( - compassDirection: Wind.CompassDirection(value: daytimeForecast.windDirection), + compassDirection: Wind.CompassDirection(value: Double(daytimeForecast.windDirection)), direction: Measurement(value: Double(daytimeForecast.windDirection), unit: .degrees), - speed: Measurement(value: daytimeForecast.windSpeed, unit: .kilometersPerHour)) + speed: Measurement(value: windSpeedAvg, unit: .kilometersPerHour), + gust: Measurement(value: windGustSpeedMax, unit: .kilometersPerHour) + ), + highWindSpeed: Measurement(value: windSpeedMax, unit: .kilometersPerHour), + daytimeForecast: daytimeForecast.dayPartForecast, + overnightForecast: overnightForecast.dayPartForecast, + restOfDayForecast: restOfDayForecast?.dayPartForecast + ) + } +} + +extension APIForecast { + var dayPartForecast: DayPartForecast { + DayPartForecast( + cloudCover: cloudCover.percentage, + cloudCoverByAltitude: CloudCoverByAltitude( + low: cloudCoverLowAltPct.percentage, + medium: cloudCoverMidAltPct.percentage, + high: cloudCoverHighAltPct.percentage + ), + condition: WeatherCondition(rawValue: conditionCode) ?? .undefined, + highTemperature: Measurement(value: temperatureMax, unit: .celsius), + lowTemperature: Measurement(value: temperatureMin, unit: .celsius), + precipitation: Precipitation(rawValue: precipitationType) ?? .none, + precipitationAmountByType: precipitationAmountByType.precipitationAmountByType, + precipitationChance: precipitationChance.percentage, + maximumHumidity: humidityMax.percentage, + minimumHumidity: humidityMin.percentage, + maximumVisibility: Measurement(value: visibilityMax, unit: .meters), + minimumVisibility: Measurement(value: visibilityMin, unit: .meters), + wind: Wind( + compassDirection: Wind.CompassDirection(value: Double(windDirection)), + direction: Measurement(value: Double(windDirection), unit: .degrees), + speed: Measurement(value: windSpeed, unit: .kilometersPerHour), + gust: Measurement(value: windGustSpeedMax, unit: .kilometersPerHour) + ), + highWindSpeed: Measurement(value: windSpeedMax, unit: .kilometersPerHour) ) } } diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIForecastHourly+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIForecastHourly+Map.swift index 358b871..06b2733 100644 --- a/Sources/OpenWeatherKit/Internal/Extensions/APIForecastHourly+Map.swift +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIForecastHourly+Map.swift @@ -1,6 +1,6 @@ // // APIForecastHourly+Map.swift -// +// // // Created by Jeremy Greenwood on 10/25/22. // @@ -20,15 +20,21 @@ extension APIHour { var hourWeather: HourWeather { HourWeather( date: forecastStart, - cloudCover: cloudCover, + cloudCover: cloudCover.percentage, + cloudCoverByAltitude: CloudCoverByAltitude( + low: cloudCoverLowAltPct.percentage, + medium: cloudCoverMidAltPct.percentage, + high: cloudCoverHighAltPct.percentage + ), condition: WeatherCondition(rawValue: conditionCode) ?? .undefined, symbolName: (WeatherCondition(rawValue: conditionCode) ?? .undefined).sfSymbol, dewPoint: Measurement(value: temperatureDewPoint, unit: .celsius), - humidity: humidity, + humidity: humidity.percentage, isDaylight: daylight, precipitation: Precipitation(rawValue: precipitationType) ?? .none, - precipitationChance: precipitationChance, + precipitationChance: precipitationChance.percentage, precipitationAmount: Measurement(value: precipitationAmount, unit: .millimeters), + precipitationIntensity: Measurement(value: perceivedPrecipitationIntensity, unit: .millimetersPerHour), pressure: Measurement(value: pressure, unit: .millibars), pressureTrend: PressureTrend(rawValue: pressureTrend) ?? .undefined, snowfallIntensity: Measurement(value: snowfallIntensity ?? 0.0, unit: .millimetersPerHour), diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIForecastNextHour+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIForecastNextHour+Map.swift index 2829ba0..6f35671 100644 --- a/Sources/OpenWeatherKit/Internal/Extensions/APIForecastNextHour+Map.swift +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIForecastNextHour+Map.swift @@ -1,6 +1,6 @@ // // APIForecastNextHour+Map.swift -// +// // // Created by Jeremy Greenwood on 10/25/22. // @@ -9,18 +9,33 @@ import Foundation extension APIForecastNextHour { var minuteForecast: Forecast { - Forecast( - forecast: minutes.map(\.minuteWeather), + var _conditions = condition + var _condition: APICondition? = nil + + return Forecast( + forecast: minutes.map { minute in + // Find the condition that applies to this minute, if any. + // Conditions are sorted by start time, so we can just check the first one. + // If it applies, remove it from the list so we don't check it again. + // This assumes that conditions do not overlap. + if let condition = _conditions.first, condition.startTime <= minute.startTime { + _condition = condition + _conditions.removeFirst() + } + + // Map the minute and its condition to a MinuteWeather. + return minute.minuteWeather(_condition) + }, metadata: metadata.weatherMetadata ) } } extension APIMinute { - var minuteWeather: MinuteWeather { + func minuteWeather(_ condition: APICondition?) -> MinuteWeather { MinuteWeather( date: startTime, - precipitation: condition != nil ? (Precipitation(rawValue: condition!) ?? .none) : .none, + precipitation: condition != nil ? (Precipitation(rawValue: condition!.beginCondition) ?? .none) : .none, precipitationChance: precipitationChance, precipitationIntensity: Measurement(value: precipitationIntensity, unit: .metersPerSecond) ) diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIWeatherAlerts+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIWeatherAlerts+Map.swift index 248b256..b960c69 100644 --- a/Sources/OpenWeatherKit/Internal/Extensions/APIWeatherAlerts+Map.swift +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIWeatherAlerts+Map.swift @@ -1,6 +1,6 @@ // // APIWeatherAlerts+Map.swift -// +// // // Created by Jeremy Greenwood on 10/26/22. // @@ -22,7 +22,6 @@ extension APIWeatherAlerts { extension APIAlertSummary { var alertSummary: AlertSummary { AlertSummary( - name: name, id: id, areaID: areaID, areaName: areaName, diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIWeatherAvailability+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIWeatherAvailability+Map.swift index f71a7cd..1064428 100644 --- a/Sources/OpenWeatherKit/Internal/Extensions/APIWeatherAvailability+Map.swift +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIWeatherAvailability+Map.swift @@ -1,6 +1,6 @@ // // APIWeatherAvailability+Map.swift -// +// // // Created by Jeremy Greenwood on 10/27/22. // @@ -11,7 +11,7 @@ extension Array where Element == APIWeatherAvailability { var weatherAvailability: WeatherAvailability { WeatherAvailability( minuteAvailability: contains(.forecastNextHour) ? .available : .temporarilyUnavailable, - alertAvailability: contains(.weatherAlerts) ? .available :.temporarilyUnavailable + alertAvailability: contains(.weatherAlerts) ? .available : .temporarilyUnavailable ) } diff --git a/Sources/OpenWeatherKit/Internal/Extensions/Int+Utils.swift b/Sources/OpenWeatherKit/Internal/Extensions/Int+Utils.swift new file mode 100644 index 0000000..9e0b164 --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Extensions/Int+Utils.swift @@ -0,0 +1,12 @@ +// +// Int+Utils.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 2/21/25. +// + +import Foundation + +extension Int { + var percentage: Double { Double(self) / 100 } +} diff --git a/Sources/OpenWeatherKit/Internal/Extensions/Measurement+Utils.swift b/Sources/OpenWeatherKit/Internal/Extensions/Measurement+Utils.swift new file mode 100644 index 0000000..b230ecf --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Extensions/Measurement+Utils.swift @@ -0,0 +1,18 @@ +// +// Measurement+Utils.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 2/21/25. +// + +import Foundation + +extension Measurement where UnitType == UnitLength { + static var zeroMillimeters: Measurement { + Measurement(value: 0.0, unit: .millimeters) + } + + static func millimeters(_ value: Double) -> Measurement { + Measurement(value: value, unit: .millimeters) + } +} diff --git a/Sources/OpenWeatherKit/Internal/Extensions/WeatherQuery+QueryItems.swift b/Sources/OpenWeatherKit/Internal/Extensions/WeatherQuery+QueryItems.swift index 40e1122..3a3613b 100644 --- a/Sources/OpenWeatherKit/Internal/Extensions/WeatherQuery+QueryItems.swift +++ b/Sources/OpenWeatherKit/Internal/Extensions/WeatherQuery+QueryItems.swift @@ -1,6 +1,6 @@ // // WeatherQuery+QueryItems.swift -// +// // // Created by Jeremy Greenwood on 10/28/22. // @@ -15,6 +15,7 @@ enum QueryContants { static let dataSets = "dataSets" static let hourlyEnd = "hourlyEnd" static let hourlyStart = "hourlyStart" + static let timezone = "timezone" } extension Array where Element == Query { diff --git a/Sources/OpenWeatherKit/Internal/Geocoder.swift b/Sources/OpenWeatherKit/Internal/Geocoder.swift index 2d80f29..804384e 100644 --- a/Sources/OpenWeatherKit/Internal/Geocoder.swift +++ b/Sources/OpenWeatherKit/Internal/Geocoder.swift @@ -1,6 +1,6 @@ // // Geocoder.swift -// +// // // Created by Jeremy Greenwood on 11/17/22. // @@ -13,17 +13,29 @@ import Foundation struct Geocoder: Sendable { @usableFromInline var countryCode: @Sendable (LocationProtocol) async throws -> String? + @usableFromInline + var timezone: @Sendable (LocationProtocol) async throws -> String? static var live: Self { let geocoder = CLGeocoder() - return .init { - try await geocoder.reverseGeocodeLocation( - CLLocation( - latitude: $0.latitude, - longitude: $0.longitude - ) - ).first?.isoCountryCode - } + return .init( + countryCode: { + try await geocoder.reverseGeocodeLocation( + CLLocation( + latitude: $0.latitude, + longitude: $0.longitude + ) + ).first?.isoCountryCode + }, + timezone: { + try await geocoder.reverseGeocodeLocation( + CLLocation( + latitude: $0.latitude, + longitude: $0.longitude + ) + ).first?.timeZone?.identifier + } + ) } } #endif diff --git a/Sources/OpenWeatherKit/Internal/Models/APICurrentWeather.swift b/Sources/OpenWeatherKit/Internal/Models/APICurrentWeather.swift index b47d088..eefa480 100644 --- a/Sources/OpenWeatherKit/Internal/Models/APICurrentWeather.swift +++ b/Sources/OpenWeatherKit/Internal/Models/APICurrentWeather.swift @@ -9,16 +9,15 @@ import Foundation // MARK: - APICurrentWeather struct APICurrentWeather: Codable, Equatable { - let name: String - let metadata: APIMetadata + @TextCaseCoding var conditionCode: String + @TextCaseCoding var pressureTrend: String let asOf: Date let cloudCover: Double - let conditionCode: String let daylight: Bool let humidity: Double + let metadata: APIMetadata let precipitationIntensity: Double let pressure: Double - let pressureTrend: String let temperature: Double let temperatureApparent: Double let temperatureDewPoint: Double @@ -29,13 +28,12 @@ struct APICurrentWeather: Codable, Equatable { let windSpeed: Double enum CodingKeys: String, CodingKey { - case name = "name" - case metadata = "metadata" case asOf = "asOf" case cloudCover = "cloudCover" case conditionCode = "conditionCode" case daylight = "daylight" case humidity = "humidity" + case metadata = "metadata" case precipitationIntensity = "precipitationIntensity" case pressure = "pressure" case pressureTrend = "pressureTrend" diff --git a/Sources/OpenWeatherKit/Internal/Models/APIForecastDaily.swift b/Sources/OpenWeatherKit/Internal/Models/APIForecastDaily.swift index 3a3d8fd..0b6a7f8 100644 --- a/Sources/OpenWeatherKit/Internal/Models/APIForecastDaily.swift +++ b/Sources/OpenWeatherKit/Internal/Models/APIForecastDaily.swift @@ -9,12 +9,10 @@ import Foundation // MARK: - APIForecastDaily struct APIForecastDaily: Codable, Equatable { - let name: String let metadata: APIMetadata let days: [APIDay] enum CodingKeys: String, CodingKey { - case name = "name" case metadata = "metadata" case days = "days" } @@ -22,88 +20,146 @@ struct APIForecastDaily: Codable, Equatable { // MARK: - APIDay struct APIDay: Codable, Equatable { - let forecastStart: Date + @TextCaseCoding var conditionCode: String + @TextCaseCoding var moonPhase: String + @TextCaseCoding var precipitationType: String + let daytimeForecast: APIForecast let forecastEnd: Date - let conditionCode: String + let forecastStart: Date + let humidityMax: Int + let humidityMin: Int let maxUvIndex: Int - let moonPhase: String let moonrise: Date? let moonset: Date? + let overnightForecast: APIForecast let precipitationAmount: Double - let precipitationChance: Double - let precipitationType: String - let snowfallAmount: Double - let solarMidnight: Date - let solarNoon: Date - let sunrise: Date - let sunriseCivil: Date - let sunriseNautical: Date - let sunriseAstronomical: Date - let sunset: Date - let sunsetCivil: Date - let sunsetNautical: Date - let sunsetAstronomical: Date + let precipitationAmountByType: [APIPrecipitationAmountByType] + let precipitationChance: Int + let restOfDayForecast: APIForecast? + let snowfallAmount: Int + let solarMidnight: Date? + let solarNoon: Date? + let sunrise: Date? + let sunriseAstronomical: Date? + let sunriseCivil: Date? + let sunriseNautical: Date? + let sunset: Date? + let sunsetAstronomical: Date? + let sunsetCivil: Date? + let sunsetNautical: Date? let temperatureMax: Double + let temperatureMaxTime: Date? let temperatureMin: Double - let daytimeForecast: APIForecast - let overnightForecast: APIForecast - let restOfDayForecast: APIForecast? + let temperatureMinTime: Date? + let visibilityMax: Double + let visibilityMin: Double + let windGustSpeedMax: Double + let windSpeedAvg: Double + let windSpeedMax: Double enum CodingKeys: String, CodingKey { - case forecastStart = "forecastStart" - case forecastEnd = "forecastEnd" case conditionCode = "conditionCode" + case daytimeForecast = "daytimeForecast" + case forecastEnd = "forecastEnd" + case forecastStart = "forecastStart" + case humidityMax = "humidityMax" + case humidityMin = "humidityMin" case maxUvIndex = "maxUvIndex" case moonPhase = "moonPhase" case moonrise = "moonrise" case moonset = "moonset" + case overnightForecast = "overnightForecast" case precipitationAmount = "precipitationAmount" + case precipitationAmountByType = "precipitationAmountByType" case precipitationChance = "precipitationChance" case precipitationType = "precipitationType" + case restOfDayForecast = "restOfDayForecast" case snowfallAmount = "snowfallAmount" case solarMidnight = "solarMidnight" case solarNoon = "solarNoon" case sunrise = "sunrise" + case sunriseAstronomical = "sunriseAstronomical" case sunriseCivil = "sunriseCivil" case sunriseNautical = "sunriseNautical" - case sunriseAstronomical = "sunriseAstronomical" case sunset = "sunset" + case sunsetAstronomical = "sunsetAstronomical" case sunsetCivil = "sunsetCivil" case sunsetNautical = "sunsetNautical" - case sunsetAstronomical = "sunsetAstronomical" case temperatureMax = "temperatureMax" + case temperatureMaxTime = "temperatureMaxTime" case temperatureMin = "temperatureMin" - case daytimeForecast = "daytimeForecast" - case overnightForecast = "overnightForecast" - case restOfDayForecast = "restOfDayForecast" + case temperatureMinTime = "temperatureMinTime" + case visibilityMax = "visibilityMax" + case visibilityMin = "visibilityMin" + case windGustSpeedMax = "windGustSpeedMax" + case windSpeedAvg = "windSpeedAvg" + case windSpeedMax = "windSpeedMax" } } // MARK: - APIForecast struct APIForecast: Codable, Equatable { - let forecastStart: Date - let forecastEnd: Date - let cloudCover: Double + let cloudCover: Int + let cloudCoverHighAltPct: Int + let cloudCoverLowAltPct: Int + let cloudCoverMidAltPct: Int let conditionCode: String - let humidity: Double + let daylight: Bool + let forecastEnd: Int + let forecastStart: Int + let humidity: Int + let humidityMax: Int + let humidityMin: Int + let perceivedPrecipitationIntensityMax: Double let precipitationAmount: Double - let precipitationChance: Double + let precipitationAmountByType: [APIPrecipitationAmountByType] + let precipitationChance: Int + let precipitationIntensityMax: Double let precipitationType: String - let snowfallAmount: Double - let windDirection: Double + let snowfallAmount: Int + let temperatureApparentMax: Double + let temperatureApparentMin: Double + let temperatureMax: Double + let temperatureMin: Double + let uvIndexMax: Int + let uvIndexMin: Int + let visibilityMax: Double + let visibilityMin: Double + let windDirection: Int + let windGustSpeedMax: Double let windSpeed: Double + let windSpeedMax: Double enum CodingKeys: String, CodingKey { - case forecastStart = "forecastStart" - case forecastEnd = "forecastEnd" case cloudCover = "cloudCover" + case cloudCoverHighAltPct = "cloudCoverHighAltPct" + case cloudCoverLowAltPct = "cloudCoverLowAltPct" + case cloudCoverMidAltPct = "cloudCoverMidAltPct" case conditionCode = "conditionCode" + case daylight = "daylight" + case forecastEnd = "forecastEnd" + case forecastStart = "forecastStart" case humidity = "humidity" + case humidityMax = "humidityMax" + case humidityMin = "humidityMin" + case perceivedPrecipitationIntensityMax = "perceivedPrecipitationIntensityMax" case precipitationAmount = "precipitationAmount" + case precipitationAmountByType = "precipitationAmountByType" case precipitationChance = "precipitationChance" + case precipitationIntensityMax = "precipitationIntensityMax" case precipitationType = "precipitationType" case snowfallAmount = "snowfallAmount" + case temperatureApparentMax = "temperatureApparentMax" + case temperatureApparentMin = "temperatureApparentMin" + case temperatureMax = "temperatureMax" + case temperatureMin = "temperatureMin" + case uvIndexMax = "uvIndexMax" + case uvIndexMin = "uvIndexMin" + case visibilityMax = "visibilityMax" + case visibilityMin = "visibilityMin" case windDirection = "windDirection" + case windGustSpeedMax = "windGustSpeedMax" case windSpeed = "windSpeed" + case windSpeedMax = "windSpeedMax" } } diff --git a/Sources/OpenWeatherKit/Internal/Models/APIForecastHourly.swift b/Sources/OpenWeatherKit/Internal/Models/APIForecastHourly.swift index e7f743f..24adcd3 100644 --- a/Sources/OpenWeatherKit/Internal/Models/APIForecastHourly.swift +++ b/Sources/OpenWeatherKit/Internal/Models/APIForecastHourly.swift @@ -9,12 +9,10 @@ import Foundation // MARK: - APIForecastHourly struct APIForecastHourly: Codable, Equatable { - let name: String let metadata: APIMetadata let hours: [APIHour] enum CodingKeys: String, CodingKey { - case name = "name" case metadata = "metadata" case hours = "hours" } @@ -22,19 +20,23 @@ struct APIForecastHourly: Codable, Equatable { // MARK: - APIHour struct APIHour: Codable, Equatable { - let forecastStart: Date - let cloudCover: Double - let conditionCode: String + @TextCaseCoding var conditionCode: String + @TextCaseCoding var precipitationType: String + @TextCaseCoding var pressureTrend: String + let cloudCover: Int + let cloudCoverHighAltPct: Int + let cloudCoverLowAltPct: Int + let cloudCoverMidAltPct: Int let daylight: Bool - let humidity: Double + let forecastStart: Date + let humidity: Int + let perceivedPrecipitationIntensity: Double let precipitationAmount: Double + let precipitationChance: Int let precipitationIntensity: Double - let precipitationChance: Double - let precipitationType: String let pressure: Double - let pressureTrend: String - let snowfallIntensity: Double? let snowfallAmount: Double? + let snowfallIntensity: Double? let temperature: Double let temperatureApparent: Double let temperatureDewPoint: Double @@ -47,9 +49,13 @@ struct APIHour: Codable, Equatable { enum CodingKeys: String, CodingKey { case forecastStart = "forecastStart" case cloudCover = "cloudCover" + case cloudCoverHighAltPct = "cloudCoverHighAltPct" + case cloudCoverLowAltPct = "cloudCoverLowAltPct" + case cloudCoverMidAltPct = "cloudCoverMidAltPct" case conditionCode = "conditionCode" case daylight = "daylight" case humidity = "humidity" + case perceivedPrecipitationIntensity = "perceivedPrecipitationIntensity" case precipitationAmount = "precipitationAmount" case precipitationIntensity = "precipitationIntensity" case precipitationChance = "precipitationChance" diff --git a/Sources/OpenWeatherKit/Internal/Models/APIForecastNextHour.swift b/Sources/OpenWeatherKit/Internal/Models/APIForecastNextHour.swift index 967d445..fbb9f49 100644 --- a/Sources/OpenWeatherKit/Internal/Models/APIForecastNextHour.swift +++ b/Sources/OpenWeatherKit/Internal/Models/APIForecastNextHour.swift @@ -1,6 +1,6 @@ // // APIForecastNextHour.swift -// +// // // Created by Jeremy Greenwood on 10/16/22. // @@ -9,34 +9,40 @@ import Foundation // MARK: - APIForecastNextHour struct APIForecastNextHour: Codable, Equatable { - let name: String - let metadata: APIMetadata - let summary: [APIMinute] - let forecastStart: Date + let condition: [APICondition] let forecastEnd: Date + let forecastStart: Date + let metadata: APIMetadata let minutes: [APIMinute] + let summary: [APIMinute] enum CodingKeys: String, CodingKey { - case name = "name" - case metadata = "metadata" - case summary = "summary" - case forecastStart = "forecastStart" + case condition = "condition" case forecastEnd = "forecastEnd" + case forecastStart = "forecastStart" + case metadata = "metadata" case minutes = "minutes" + case summary = "summary" } } // MARK: - APIMinute struct APIMinute: Codable, Equatable { - let startTime: Date let precipitationChance: Double let precipitationIntensity: Double - let condition: String? + let startTime: Date enum CodingKeys: String, CodingKey { - case startTime = "startTime" case precipitationChance = "precipitationChance" case precipitationIntensity = "precipitationIntensity" - case condition = "condition" + case startTime = "startTime" } } + +// MARK: - APICondition +struct APICondition: Codable, Equatable { + @TextCaseCoding var beginCondition: String + @TextCaseCoding var endCondition: String + @TextCaseCoding var forecastToken: String + let startTime: Date +} \ No newline at end of file diff --git a/Sources/OpenWeatherKit/Internal/Models/APIMetadata.swift b/Sources/OpenWeatherKit/Internal/Models/APIMetadata.swift index 8458536..1df1288 100644 --- a/Sources/OpenWeatherKit/Internal/Models/APIMetadata.swift +++ b/Sources/OpenWeatherKit/Internal/Models/APIMetadata.swift @@ -1,6 +1,6 @@ // // APIMetadata.swift -// +// // // Created by Jeremy Greenwood on 9/4/22. // @@ -9,27 +9,27 @@ import Foundation // MARK: - APIMetadata struct APIMetadata: Codable, Equatable { + @TextCaseCoding var sourceType: String let attributionURL: String let expireTime: Date + let language: String? let latitude: Double let longitude: Double + let providerName: String? let readTime: Date let reportedTime: Date? - let units: String? - let version: Int - let language: String? - let providerName: String? + let temporarilyUnavailable: Bool enum CodingKeys: String, CodingKey { - case attributionURL = "attributionURL" + case attributionURL = "attributionUrl" case expireTime = "expireTime" + case language = "language" case latitude = "latitude" case longitude = "longitude" + case providerName = "providerName" case readTime = "readTime" case reportedTime = "reportedTime" - case units = "units" - case version = "version" - case language = "language" - case providerName = "providerName" + case sourceType = "sourceType" + case temporarilyUnavailable = "temporarilyUnavailable" } } diff --git a/Sources/OpenWeatherKit/Internal/Models/APIPrecipitationAmountByType.swift b/Sources/OpenWeatherKit/Internal/Models/APIPrecipitationAmountByType.swift new file mode 100644 index 0000000..74c694f --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Models/APIPrecipitationAmountByType.swift @@ -0,0 +1,62 @@ +// +// APIPrecipitationAmountByType.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 2/21/25. +// + +import Foundation + +// MARK: - APIPrecipitationAmountByType +struct APIPrecipitationAmountByType: Codable, Equatable { + @TextCaseCoding var precipitationType: String + let expected: Double + let expectedSnow: Double + let maximumSnow: Double + let minimumSnow: Double +} + +extension Array where Element == APIPrecipitationAmountByType { + var precipitationAmountByType: PrecipitationAmountByType { + var amountByType = PrecipitationAmountByType( + hail: .zeroMillimeters, + mixed: .zeroMillimeters, + rainfall: .zeroMillimeters, + sleet: .zeroMillimeters, + precipitation: .zeroMillimeters, + snowfallAmount: SnowfallAmount( + amount: .zeroMillimeters, + maximum: .zeroMillimeters, + minimum: .zeroMillimeters, + amountLiquidEquivalent: .zeroMillimeters, + maximumLiquidEquivalent: .zeroMillimeters, + minimumLiquidEquivalent: .zeroMillimeters + ) + ) + + for element in self { + switch element.precipitationType { + case "hail": + amountByType.hail = .millimeters(element.expected) + case "mixed": + amountByType.mixed = .millimeters(element.expected) + case "rain": + amountByType.rainfall = .millimeters(element.expected) + case "sleet": + amountByType.sleet = .millimeters(element.expected) + case "snow": + amountByType.snowfallAmount = SnowfallAmount( + amount: .millimeters(element.expectedSnow), + maximum: .millimeters(element.maximumSnow), + minimum: .millimeters(element.minimumSnow), + amountLiquidEquivalent: .millimeters(element.expectedSnow / 10.0), // assuming 10: snow to liquid ratio + maximumLiquidEquivalent: .millimeters(element.maximumSnow / 10.0), + minimumLiquidEquivalent: .millimeters(element.minimumSnow / 10.0) + ) + default: break + } + } + + return amountByType + } +} diff --git a/Sources/OpenWeatherKit/Internal/Models/APIWeatherAlerts.swift b/Sources/OpenWeatherKit/Internal/Models/APIWeatherAlerts.swift index 92db0a0..f4aa152 100644 --- a/Sources/OpenWeatherKit/Internal/Models/APIWeatherAlerts.swift +++ b/Sources/OpenWeatherKit/Internal/Models/APIWeatherAlerts.swift @@ -1,6 +1,6 @@ // // APIWeatherAlerts.swift -// +// // // Created by Jeremy Greenwood on 10/16/22. // @@ -9,13 +9,11 @@ import Foundation // MARK: - APIWeatherAlerts struct APIWeatherAlerts: Codable, Equatable { - let name: String let metadata: APIMetadata let detailsURL: URL let alerts: [APIAlertSummary] enum CodingKeys: String, CodingKey { - case name = "name" case metadata = "metadata" case detailsURL = "detailsUrl" case alerts = "alerts" @@ -23,88 +21,95 @@ struct APIWeatherAlerts: Codable, Equatable { } struct APIAlertSummary: Codable, Equatable { - let name: String - let id: String + @TextCaseCoding var certainty: String + @TextCaseCoding var importance: String + @TextCaseCoding var severity: String + @TextCaseCoding var significance: String + @TextCaseCoding var urgency: String + @TextCaseCoding var responses: [String] + let alertDescription: String let areaID: String? let areaName: String? let attributionURL: String let countryCode: String - let alertDescription: String + let detailsURL: URL let effectiveTime: Date + let eventOnsetTime: Date + let eventSource: String let expireTime: Date + let id: String let issuedTime: Date - let detailsURL: URL let phenomenon: String - let severity: String let source: String - let eventSource: String - let urgency: String - let certainty: String - let importance: String - let responses: [String] + let token: String enum CodingKeys: String, CodingKey { - case name = "name" - case id = "id" + case alertDescription = "description" case areaID = "areaId" case areaName = "areaName" - case attributionURL = "attributionURL" + case attributionURL = "attributionUrl" + case certainty = "certainty" case countryCode = "countryCode" - case alertDescription = "description" + case detailsURL = "detailsUrl" case effectiveTime = "effectiveTime" + case eventOnsetTime = "eventOnsetTime" + case eventSource = "eventSource" case expireTime = "expireTime" + case id = "id" + case importance = "importance" case issuedTime = "issuedTime" - case detailsURL = "detailsUrl" case phenomenon = "phenomenon" + case responses = "responses" case severity = "severity" + case significance = "significance" case source = "source" - case eventSource = "eventSource" + case token = "token" case urgency = "urgency" - case certainty = "certainty" - case importance = "importance" - case responses = "responses" } init( - name: String, - id: String, - areaID: String, - areaName: String, + alertDescription: String, + areaID: String?, + areaName: String?, attributionURL: String, + certainty: String, countryCode: String, - alertDescription: String, + detailsURL: URL, effectiveTime: Date, + eventOnsetTime: Date, + eventSource: String, expireTime: Date, + id: String, + importance: String, issuedTime: Date, - detailsURL: URL, phenomenon: String, - alertPrecedence: Int, + responses: [String], severity: String, + significance: String, source: String, - eventSource: String, - urgency: String, - certainty: String, - importance: String, - responses: [String] + token: String, + urgency: String ) { - self.name = name - self.id = id + self.alertDescription = alertDescription self.areaID = areaID self.areaName = areaName self.attributionURL = attributionURL + self.certainty = certainty self.countryCode = countryCode - self.alertDescription = alertDescription + self.detailsURL = detailsURL self.effectiveTime = effectiveTime + self.eventOnsetTime = eventOnsetTime + self.eventSource = eventSource self.expireTime = expireTime + self.id = id + self.importance = importance self.issuedTime = issuedTime - self.detailsURL = detailsURL self.phenomenon = phenomenon + self.responses = responses self.severity = severity + self.significance = significance self.source = source - self.eventSource = eventSource + self.token = token self.urgency = urgency - self.certainty = certainty - self.importance = importance - self.responses = responses } } diff --git a/Sources/OpenWeatherKit/Internal/Models/TextCaseCoding.swift b/Sources/OpenWeatherKit/Internal/Models/TextCaseCoding.swift new file mode 100644 index 0000000..37ddbbf --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Models/TextCaseCoding.swift @@ -0,0 +1,83 @@ +// +// TextCaseCoding.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 2/24/25. +// + +import Foundation + +@propertyWrapper +public struct TextCaseCoding: Codable, Sendable { + public var wrappedValue: Case.Value + + public init(wrappedValue: Case.Value) { + self.wrappedValue = wrappedValue + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(Case.transform(wrappedValue)) + } +} + +// swiftlint:disable force_cast +public extension KeyedDecodingContainer { + func decode(_ type: TextCaseCoding.Type, forKey key: Key) throws -> TextCaseCoding { + if Case.Value.self is Optional.Type { + try TextCaseCoding(wrappedValue: Case.transform(decodeIfPresent(String.self, forKey: key) as! Case.Value)) + } else if Case.Value.self is String.Type { + try TextCaseCoding(wrappedValue: Case.transform(decode(String.self, forKey: key) as! Case.Value)) + } else if Case.Value.self is Array.Type { + try TextCaseCoding(wrappedValue: Case.transform(decode([String].self, forKey: key) as! Case.Value)) + } else { + throw DecodingError.dataCorrupted(.init(codingPath: [key], debugDescription: "Not Implemented")) + } + } +} +// swiftlint:enable force_cast + +extension TextCaseCoding: Equatable where Case.Value: Equatable { } +extension TextCaseCoding: Hashable where Case.Value: Hashable { } + +public protocol TextCase { + associatedtype Value: Codable, Sendable + + static var transform: @Sendable (Value) -> Value { get } +} + +public enum Lowercased: TextCase { + public static let transform: @Sendable (String) -> String = { $0.lowercased() } +} + +public enum Uppercased: TextCase { + public static let transform: @Sendable (String) -> String = { $0.uppercased() } +} + +public enum Capitalized: TextCase { + public static let transform: @Sendable (String) -> String = { $0.capitalized } +} + +public enum LowercasedOptional: TextCase { + public static let transform: @Sendable (String?) -> String? = { $0?.lowercased() } +} + +public enum UppercasedOptional: TextCase { + public static let transform: @Sendable (String?) -> String? = { $0?.uppercased() } +} + +public enum CapitalizedOptional: TextCase { + public static let transform: @Sendable (String?) -> String? = { $0?.capitalized } +} + +public enum LowercasedArray: TextCase { + public static let transform: @Sendable ([String]) -> [String] = { $0.map { $0.lowercased() } } +} + +public enum UppercasedArray: TextCase { + public static let transform: @Sendable ([String]) -> [String] = { $0.map { $0.uppercased() } } +} + +public enum CapitalizedArray: TextCase { + public static let transform: @Sendable ([String]) -> [String] = { $0.map(\.capitalized) } +} diff --git a/Sources/OpenWeatherKit/Internal/NetworkClient.swift b/Sources/OpenWeatherKit/Internal/NetworkClient.swift index 154f84e..c28be45 100644 --- a/Sources/OpenWeatherKit/Internal/NetworkClient.swift +++ b/Sources/OpenWeatherKit/Internal/NetworkClient.swift @@ -1,6 +1,6 @@ // // NetworkClient.swift -// +// // // Created by Jeremy Greenwood on 10/31/22. // @@ -39,6 +39,7 @@ struct NetworkClient: Sendable { location: LocationProtocol, language: WeatherService.Configuration.Language, queries: Query..., + timezone: TimeZone, jwt: String ) async throws -> WeatherProxy { try await withThrowingTaskGroup(of: WeatherProxy.self) { group in @@ -63,7 +64,17 @@ struct NetworkClient: Sendable { // if queries other than availability if !_queries.isEmpty { let queryItems = _queries.queryItems + group.addTask { + // add timezone query item + var queryItems = queryItems + queryItems.append( + URLQueryItem( + name: QueryContants.timezone, + value: timezone.identifier + ) + ) + let weather: APIWeather = try await get( .weather(language, location), queryItems: queryItems, @@ -114,7 +125,7 @@ extension NetworkClient { #endif let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 + decoder.dateDecodingStrategy = .secondsSince1970 return try decoder.decode(T.self, from: data) } diff --git a/Sources/OpenWeatherKit/Internal/Route.swift b/Sources/OpenWeatherKit/Internal/Route.swift index 4aafb45..06a3ac1 100644 --- a/Sources/OpenWeatherKit/Internal/Route.swift +++ b/Sources/OpenWeatherKit/Internal/Route.swift @@ -1,6 +1,6 @@ // // Route.swift -// +// // // Created by Jeremy Greenwood on 10/28/22. // @@ -16,8 +16,8 @@ enum Route { let base = "https://weatherkit.apple.com" switch self { - case let .availability(location): return "\(base)/api/v1/availability/\(location.latitude)/\(location.longitude)" - case let .weather(language, location): return "\(base)/api/v1/weather/\(language.rawValue)/\(location.latitude)/\(location.longitude)" + case let .availability(location): return "\(base)/api/v2/availability/\(location.latitude)/\(location.longitude)" + case let .weather(language, location): return "\(base)/api/v2/weather/\(language.rawValue)/\(location.latitude)/\(location.longitude)" } }() diff --git a/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift b/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift index 917030e..eed7c82 100644 --- a/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift +++ b/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift @@ -10,69 +10,50 @@ import Foundation /// An enumeration that specifies the moon phase kind. @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @frozen public enum MoonPhase : String, CustomStringConvertible, CaseIterable { - enum CodingKeys: String, CodingKey { - case new, waxingCrescent, firstQuarter, waxingGibbous, full, waningGibbous, waningCrescent - case lastQuarter = "thirdQuarter" - } /// The disk is unlit where the moon is not visible. - case new + case new = "new" /// The disk is partially lit as the moon is waxing. - case waxingCrescent + case waxingCrescent = "waxing_crescent" /// The disk is half lit. - case firstQuarter + case firstQuarter = "first_quarter" /// The disk is half lit as the moon is waxing. - case waxingGibbous + case waxingGibbous = "waxing_gibbous" /// The disk is fully lit where the moon is visible. - case full + case full = "full" /// The disk is half lit as the moon is waning. - case waningGibbous + case waningGibbous = "waning_gibbous" /// The disk is half lit. - case lastQuarter + case lastQuarter = "last_quarter" /// The disk is partially lit as the moon is waning. - case waningCrescent - - public init?(rawValue: String) { - switch rawValue { - case CodingKeys.new.rawValue: self = .new - case CodingKeys.waxingCrescent.rawValue: self = .waxingCrescent - case CodingKeys.firstQuarter.rawValue: self = .firstQuarter - case CodingKeys.waxingGibbous.rawValue: self = .waxingGibbous - case CodingKeys.full.rawValue: self = .full - case CodingKeys.waningGibbous.rawValue: self = .waningGibbous - case CodingKeys.lastQuarter.rawValue: self = .lastQuarter - case CodingKeys.waningCrescent.rawValue: self = .waningCrescent - default: - return nil - } - } + case waningCrescent = "waning_crescent" /// A localized string describing the moon phase. public var description: String { switch self { case .new: - return NSLocalizedString("MoonPhase.new", bundle: Bundle.main, comment: "New") + return NSLocalizedString("MoonPhase.new", bundle: Bundle.module, comment: "New") case .waxingCrescent: - return NSLocalizedString("MoonPhase.waxingCrescent", bundle: Bundle.main, comment: "WaxingCrescent") + return NSLocalizedString("MoonPhase.waxingCrescent", bundle: Bundle.module, comment: "WaxingCrescent") case .firstQuarter: - return NSLocalizedString("MoonPhase.firstQuarter", bundle: Bundle.main, comment: "FirstQuarter") + return NSLocalizedString("MoonPhase.firstQuarter", bundle: Bundle.module, comment: "FirstQuarter") case .waxingGibbous: - return NSLocalizedString("MoonPhase.waxingGibbous", bundle: Bundle.main, comment: "WaxingGibbous") + return NSLocalizedString("MoonPhase.waxingGibbous", bundle: Bundle.module, comment: "WaxingGibbous") case .full: - return NSLocalizedString("MoonPhase.full", bundle: Bundle.main, comment: "Full") + return NSLocalizedString("MoonPhase.full", bundle: Bundle.module, comment: "Full") case .waningGibbous: - return NSLocalizedString("MoonPhase.waningGibbous", bundle: Bundle.main, comment: "WaningGibbous") + return NSLocalizedString("MoonPhase.waningGibbous", bundle: Bundle.module, comment: "WaningGibbous") case .lastQuarter: - return NSLocalizedString("MoonPhase.lastQuarter", bundle: Bundle.main, comment: "LastQuarter") + return NSLocalizedString("MoonPhase.lastQuarter", bundle: Bundle.module, comment: "LastQuarter") case .waningCrescent: - return NSLocalizedString("MoonPhase.waningCrescent", bundle: Bundle.main, comment: "WaningCrescent") + return NSLocalizedString("MoonPhase.waningCrescent", bundle: Bundle.module, comment: "WaningCrescent") } } @@ -81,21 +62,21 @@ import Foundation public var accessibilityDescription: String { switch self { case .new: - return NSLocalizedString("MoonPhase.accessibility.new", bundle: Bundle.main, comment: "New") + return NSLocalizedString("MoonPhase.accessibility.new", bundle: Bundle.module, comment: "New") case .waxingCrescent: - return NSLocalizedString("MoonPhase.accessibility.waxingCrescent", bundle: Bundle.main, comment: "WaxingCrescent") + return NSLocalizedString("MoonPhase.accessibility.waxingCrescent", bundle: Bundle.module, comment: "WaxingCrescent") case .firstQuarter: - return NSLocalizedString("MoonPhase.accessibility.firstQuarter", bundle: Bundle.main, comment: "FirstQuarter") + return NSLocalizedString("MoonPhase.accessibility.firstQuarter", bundle: Bundle.module, comment: "FirstQuarter") case .waxingGibbous: - return NSLocalizedString("MoonPhase.accessibility.waxingGibbous", bundle: Bundle.main, comment: "WaxingGibbous") + return NSLocalizedString("MoonPhase.accessibility.waxingGibbous", bundle: Bundle.module, comment: "WaxingGibbous") case .full: - return NSLocalizedString("MoonPhase.accessibility.full", bundle: Bundle.main, comment: "Full") + return NSLocalizedString("MoonPhase.accessibility.full", bundle: Bundle.module, comment: "Full") case .waningGibbous: - return NSLocalizedString("MoonPhase.accessibility.waningGibbous", bundle: Bundle.main, comment: "WaningGibbous") + return NSLocalizedString("MoonPhase.accessibility.waningGibbous", bundle: Bundle.module, comment: "WaningGibbous") case .lastQuarter: - return NSLocalizedString("MoonPhase.accessibility.lastQuarter", bundle: Bundle.main, comment: "LastQuarter") + return NSLocalizedString("MoonPhase.accessibility.lastQuarter", bundle: Bundle.module, comment: "LastQuarter") case .waningCrescent: - return NSLocalizedString("MoonPhase.accessibility.waningCrescent", bundle: Bundle.main, comment: "WaningCrescent") + return NSLocalizedString("MoonPhase.accessibility.waningCrescent", bundle: Bundle.module, comment: "WaningCrescent") } } diff --git a/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift b/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift index 9d36a1b..6cc23c6 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift @@ -1,6 +1,6 @@ // // AlertSummary.swift -// +// // // Created by Jeremy Greenwood on 10/26/22. // @@ -10,7 +10,6 @@ import Foundation /// All information related to the weather alert @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct AlertSummary: Codable, Equatable, Sendable { - public var name: String public var id: String public var areaID: String? public var areaName: String? @@ -31,7 +30,6 @@ public struct AlertSummary: Codable, Equatable, Sendable { public var responses: [WeatherResponse] public init( - name: String, id: String, areaID: String?, areaName: String?, @@ -51,7 +49,6 @@ public struct AlertSummary: Codable, Equatable, Sendable { importance: String, responses: [WeatherResponse] ) { - self.name = name self.id = id self.areaID = areaID self.areaName = areaName diff --git a/Sources/OpenWeatherKit/Public/Characteristics/CloudCoverByAltitude.swift b/Sources/OpenWeatherKit/Public/Characteristics/CloudCoverByAltitude.swift new file mode 100644 index 0000000..464db61 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Characteristics/CloudCoverByAltitude.swift @@ -0,0 +1,35 @@ +// +// CloudCoverByAltitude.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 2/20/25. +// + +import Foundation + +/// +/// Contains the percentage of sky covered by low, medium and high altitude cloud. +/// +public struct CloudCoverByAltitude: Sendable { + + /// The percentage of the sky covered with low-altitude clouds. + /// Low-level Cloud Cover (LCC) corresponds to levels between 0m and 1800m above the model's earth surface. + /// + /// The value is from `0` (no cloud cover) to `1` (complete cloud cover). + public var low: Double + + /// The percentage of the sky covered with mid-altitude clouds. + /// Medium-level Cloud Cover (MCC) corresponds to levels between 1800m and 6300m above the model's earth surface. + /// + /// The value is from `0` (no cloud cover) to `1` (complete cloud cover). + public var medium: Double + + /// The percentage of the sky covered with high-altitude clouds. + /// High-level Cloud Cover (HCC)corresponds to levels higher than 6300m above the model's earth surface. + /// + /// The value is from `0` (no cloud cover) to `1` (complete cloud cover). + public var high: Double +} + +extension CloudCoverByAltitude: Codable {} +extension CloudCoverByAltitude: Equatable {} diff --git a/Sources/OpenWeatherKit/Public/Characteristics/DayPartForecast.swift b/Sources/OpenWeatherKit/Public/Characteristics/DayPartForecast.swift new file mode 100644 index 0000000..b4beb8a --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Characteristics/DayPartForecast.swift @@ -0,0 +1,70 @@ +// +// DayPartForecast.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 2/20/25. +// + +import Foundation + +/// +/// A structure that represents the weather forecast for part of the day. +/// +public struct DayPartForecast: Sendable { + + /// The percentage of the sky covered with clouds. + /// + /// The value is from `0` (no cloud cover) to `1` (complete cloud cover). + /// + public var cloudCover: Double + + /// The fraction of sky obscured by low altitude, medium altitude, and high altitude clouds. + public var cloudCoverByAltitude: CloudCoverByAltitude + + /// A description of the weather condition + public var condition: WeatherCondition + + /// The high temperature for the day part. + public var highTemperature: Measurement + + /// The overnight low temperature for the day part. + public var lowTemperature: Measurement + + /// The description of precipitation for the day part. + public var precipitation: Precipitation + + /// A breakdown of all precipitation forecasted for the day. + public var precipitationAmountByType: PrecipitationAmountByType + + /// The probability of precipitation for the day part. + /// + /// The value is from `0` (no chance of precipitation) to `1` (100% chance of precipitation). + public var precipitationChance: Double + + /// The maximum humidity for the day part. + /// Relative humidity measures the amount of water vapor in the air, compared to the maximum amount that the air can hold at the current temperature. + /// + /// The range of this property is from `0` to `1`, inclusive. + public var maximumHumidity: Double + + /// The minimum humidity for the day part. + /// Relative humidity measures the amount of water vapor in the air, compared to the maximum amount that the air can hold at the current temperature. + /// + /// The range of this property is from `0` to `1`, inclusive. + public var minimumHumidity: Double + + /// The maximum visibility for the day part. + public var maximumVisibility: Measurement + + /// The minimum visibility for the day part. + public var minimumVisibility: Measurement + + /// Wind data describing the wind speed, direction, and gust. + public var wind: Wind + + /// The maximum sustained wind speed for the day part. + public var highWindSpeed: Measurement +} + +extension DayPartForecast: Codable {} +extension DayPartForecast: Equatable {} diff --git a/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift b/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift index 1661a15..643c3b7 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift @@ -33,17 +33,17 @@ public enum Precipitation : String, CaseIterable, CustomStringConvertible, Hasha public var description: String { switch self { case .none: - return NSLocalizedString("Precipitation.none", bundle: Bundle.main, comment: "None") + return NSLocalizedString("Precipitation.none", bundle: Bundle.module, comment: "None") case .hail: - return NSLocalizedString("Precipitation.hail", bundle: Bundle.main, comment: "Hail") + return NSLocalizedString("Precipitation.hail", bundle: Bundle.module, comment: "Hail") case .mixed: - return NSLocalizedString("Precipitation.mixed", bundle: Bundle.main, comment: "Mixed") + return NSLocalizedString("Precipitation.mixed", bundle: Bundle.module, comment: "Mixed") case .rain: - return NSLocalizedString("Precipitation.rain", bundle: Bundle.main, comment: "Rain") + return NSLocalizedString("Precipitation.rain", bundle: Bundle.module, comment: "Rain") case .sleet: - return NSLocalizedString("Precipitation.sleet", bundle: Bundle.main, comment: "Sleet") + return NSLocalizedString("Precipitation.sleet", bundle: Bundle.module, comment: "Sleet") case .snow: - return NSLocalizedString("Precipitation.snow", bundle: Bundle.main, comment: "Snow") + return NSLocalizedString("Precipitation.snow", bundle: Bundle.module, comment: "Snow") } } @@ -52,17 +52,17 @@ public enum Precipitation : String, CaseIterable, CustomStringConvertible, Hasha public var accessibilityDescription: String { switch self { case .none: - return NSLocalizedString("Precipitation.accessibility.none", bundle: Bundle.main, comment: "None") + return NSLocalizedString("Precipitation.accessibility.none", bundle: Bundle.module, comment: "None") case .hail: - return NSLocalizedString("Precipitation.accessibility.hail", bundle: Bundle.main, comment: "Hail") + return NSLocalizedString("Precipitation.accessibility.hail", bundle: Bundle.module, comment: "Hail") case .mixed: - return NSLocalizedString("Precipitation.accessibility.mixed", bundle: Bundle.main, comment: "Mixed") + return NSLocalizedString("Precipitation.accessibility.mixed", bundle: Bundle.module, comment: "Mixed") case .rain: - return NSLocalizedString("Precipitation.accessibility.rain", bundle: Bundle.main, comment: "Rain") + return NSLocalizedString("Precipitation.accessibility.rain", bundle: Bundle.module, comment: "Rain") case .sleet: - return NSLocalizedString("Precipitation.accessibility.sleet", bundle: Bundle.main, comment: "Sleet") + return NSLocalizedString("Precipitation.accessibility.sleet", bundle: Bundle.module, comment: "Sleet") case .snow: - return NSLocalizedString("Precipitation.accessibility.snow", bundle: Bundle.main, comment: "Snow") + return NSLocalizedString("Precipitation.accessibility.snow", bundle: Bundle.module, comment: "Snow") } } } diff --git a/Sources/OpenWeatherKit/Public/Characteristics/PrecipitationAmountByType.swift b/Sources/OpenWeatherKit/Public/Characteristics/PrecipitationAmountByType.swift new file mode 100644 index 0000000..88ed29e --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Characteristics/PrecipitationAmountByType.swift @@ -0,0 +1,35 @@ +// +// PrecipitationAmountByType.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 2/20/25. +// + +import Foundation + +/// +/// A structure that provides a breakdown of amounts of all forms of precipitation that is expected to occur over a period of time. +/// +public struct PrecipitationAmountByType: Sendable { + + /// The amount of hail for the period. + public var hail: Measurement + + /// The amount of wintry mix for the period. + public var mixed: Measurement + + /// The amount of rainfall for the period. + public var rainfall: Measurement + + /// The amount of sleet for the period. + public var sleet: Measurement + + /// The amount of liquid equivalent of all precipitation for the period. + public var precipitation: Measurement + + /// Describes the amount of snowfall for the period. + public var snowfallAmount: SnowfallAmount +} + +extension PrecipitationAmountByType: Codable {} +extension PrecipitationAmountByType: Equatable {} diff --git a/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift b/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift index 062e3ca..298abd2 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift @@ -26,13 +26,13 @@ public enum PressureTrend : String, CaseIterable, CustomStringConvertible, Hasha public var description: String { switch self { case .rising: - return NSLocalizedString("PressureTrend.rising", bundle: Bundle.main, comment: "Rising") + return NSLocalizedString("PressureTrend.rising", bundle: Bundle.module, comment: "Rising") case .falling: - return NSLocalizedString("PressureTrend.falling", bundle: Bundle.main, comment: "Falling") + return NSLocalizedString("PressureTrend.falling", bundle: Bundle.module, comment: "Falling") case .steady: - return NSLocalizedString("PressureTrend.steady", bundle: Bundle.main, comment: "Steady") + return NSLocalizedString("PressureTrend.steady", bundle: Bundle.module, comment: "Steady") case .undefined: - return NSLocalizedString("PressureTrend.undefined", bundle: Bundle.main, comment: "Undefined") + return NSLocalizedString("PressureTrend.undefined", bundle: Bundle.module, comment: "Undefined") } } @@ -41,13 +41,13 @@ public enum PressureTrend : String, CaseIterable, CustomStringConvertible, Hasha public var accessibilityDescription: String { switch self { case .rising: - return NSLocalizedString("PressureTrend.accessibility.rising", bundle: Bundle.main, comment: "Rising") + return NSLocalizedString("PressureTrend.accessibility.rising", bundle: Bundle.module, comment: "Rising") case .falling: - return NSLocalizedString("PressureTrend.accessibility.falling", bundle: Bundle.main, comment: "Falling") + return NSLocalizedString("PressureTrend.accessibility.falling", bundle: Bundle.module, comment: "Falling") case .steady: - return NSLocalizedString("PressureTrend.accessibility.steady", bundle: Bundle.main, comment: "Steady") + return NSLocalizedString("PressureTrend.accessibility.steady", bundle: Bundle.module, comment: "Steady") case .undefined: - return NSLocalizedString("PressureTrend.accessibility.undefined", bundle: Bundle.main, comment: "Undefined") + return NSLocalizedString("PressureTrend.accessibility.undefined", bundle: Bundle.module, comment: "Undefined") } } } diff --git a/Sources/OpenWeatherKit/Public/Characteristics/SnowfallAmount.swift b/Sources/OpenWeatherKit/Public/Characteristics/SnowfallAmount.swift new file mode 100644 index 0000000..df19ecd --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Characteristics/SnowfallAmount.swift @@ -0,0 +1,35 @@ +// +// SnowfallAmount.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 2/20/25. +// + +import Foundation + +/// +/// A structure that describes the snowfall amount over a period of time. +/// +public struct SnowfallAmount: Sendable { + + /// The estimated amount of snowfall as depth of snow crystals for the period. + public var amount: Measurement + + /// The maximum amount of snowfall as depth of snow crystals for the period. + public var maximum: Measurement + + /// The minimum amount of snowfall as depth of snow crystals for the period. + public var minimum: Measurement + + /// The estimated amount of snowfall as liquid equivalent for the period. + public var amountLiquidEquivalent: Measurement + + /// The maximum amount of snowfall as liquid equivalent for the period. + public var maximumLiquidEquivalent: Measurement + + /// The minimum amount of snowfall as liquid equivalent for the period. + public var minimumLiquidEquivalent: Measurement +} + +extension SnowfallAmount: Codable {} +extension SnowfallAmount: Equatable {} diff --git a/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift b/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift index 1c10414..884591d 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift @@ -62,15 +62,15 @@ public struct UVIndex: Sendable { public var description: String { switch self { case .low: - return NSLocalizedString("UVIndex.low", bundle: Bundle.main, comment: "Low") + return NSLocalizedString("UVIndex.low", bundle: Bundle.module, comment: "Low") case .moderate: - return NSLocalizedString("UVIndex.moderate", bundle: Bundle.main, comment: "Moderate") + return NSLocalizedString("UVIndex.moderate", bundle: Bundle.module, comment: "Moderate") case .high: - return NSLocalizedString("UVIndex.high", bundle: Bundle.main, comment: "High") + return NSLocalizedString("UVIndex.high", bundle: Bundle.module, comment: "High") case .veryHigh: - return NSLocalizedString("UVIndex.veryHigh", bundle: Bundle.main, comment: "Very High") + return NSLocalizedString("UVIndex.veryHigh", bundle: Bundle.module, comment: "Very High") case .extreme: - return NSLocalizedString("UVIndex.extreme", bundle: Bundle.main, comment: "Extreme") + return NSLocalizedString("UVIndex.extreme", bundle: Bundle.module, comment: "Extreme") } } @@ -79,15 +79,15 @@ public struct UVIndex: Sendable { public var accessibilityDescription: String { switch self { case .low: - return NSLocalizedString("UVIndex.accessibility.low", bundle: Bundle.main, comment: "Low") + return NSLocalizedString("UVIndex.accessibility.low", bundle: Bundle.module, comment: "Low") case .moderate: - return NSLocalizedString("UVIndex.accessibility.moderate", bundle: Bundle.main, comment: "Moderate") + return NSLocalizedString("UVIndex.accessibility.moderate", bundle: Bundle.module, comment: "Moderate") case .high: - return NSLocalizedString("UVIndex.accessibility.high", bundle: Bundle.main, comment: "High") + return NSLocalizedString("UVIndex.accessibility.high", bundle: Bundle.module, comment: "High") case .veryHigh: - return NSLocalizedString("UVIndex.accessibility.veryHigh", bundle: Bundle.main, comment: "Very High") + return NSLocalizedString("UVIndex.accessibility.veryHigh", bundle: Bundle.module, comment: "Very High") case .extreme: - return NSLocalizedString("UVIndex.accessibility.extreme", bundle: Bundle.main, comment: "Extreme") + return NSLocalizedString("UVIndex.accessibility.extreme", bundle: Bundle.module, comment: "Extreme") } } diff --git a/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift b/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift index c993952..d31ca3f 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift @@ -86,75 +86,75 @@ public enum WeatherCondition : String, CaseIterable, CustomStringConvertible, Ha public var description: String { switch self { case .blizzard: - return NSLocalizedString("WeatherCondition.blizzard", bundle: Bundle.main, comment: "Blizzard") + return NSLocalizedString("WeatherCondition.blizzard", bundle: Bundle.module, comment: "Blizzard") case .blowingDust: - return NSLocalizedString("WeatherCondition.blowingDust", bundle: Bundle.main, comment: "Blowing Dust") + return NSLocalizedString("WeatherCondition.blowingDust", bundle: Bundle.module, comment: "Blowing Dust") case .blowingSnow: - return NSLocalizedString("WeatherCondition.blowingSnow", bundle: Bundle.main, comment: "Blowing Snow") + return NSLocalizedString("WeatherCondition.blowingSnow", bundle: Bundle.module, comment: "Blowing Snow") case .breezy: - return NSLocalizedString("WeatherCondition.breezy", bundle: Bundle.main, comment: "Breezy") + return NSLocalizedString("WeatherCondition.breezy", bundle: Bundle.module, comment: "Breezy") case .clear: - return NSLocalizedString("WeatherCondition.clear", bundle: Bundle.main, comment: "Clear") + return NSLocalizedString("WeatherCondition.clear", bundle: Bundle.module, comment: "Clear") case .cloudy: - return NSLocalizedString("WeatherCondition.cloudy", bundle: Bundle.main, comment: "Cloudy") + return NSLocalizedString("WeatherCondition.cloudy", bundle: Bundle.module, comment: "Cloudy") case .drizzle: - return NSLocalizedString("WeatherCondition.drizzle", bundle: Bundle.main, comment: "Drizzle") + return NSLocalizedString("WeatherCondition.drizzle", bundle: Bundle.module, comment: "Drizzle") case .flurries: - return NSLocalizedString("WeatherCondition.flurries", bundle: Bundle.main, comment: "Flurries") + return NSLocalizedString("WeatherCondition.flurries", bundle: Bundle.module, comment: "Flurries") case .foggy: - return NSLocalizedString("WeatherCondition.foggy", bundle: Bundle.main, comment: "Foggy") + return NSLocalizedString("WeatherCondition.foggy", bundle: Bundle.module, comment: "Foggy") case .freezingDrizzle: - return NSLocalizedString("WeatherCondition.freezingDrizzle", bundle: Bundle.main, comment: "Freezing Drizzle") + return NSLocalizedString("WeatherCondition.freezingDrizzle", bundle: Bundle.module, comment: "Freezing Drizzle") case .freezingRain: - return NSLocalizedString("WeatherCondition.freezingRain", bundle: Bundle.main, comment: "Freezing Rain") + return NSLocalizedString("WeatherCondition.freezingRain", bundle: Bundle.module, comment: "Freezing Rain") case .frigid: - return NSLocalizedString("WeatherCondition.frigid", bundle: Bundle.main, comment: "Frigid") + return NSLocalizedString("WeatherCondition.frigid", bundle: Bundle.module, comment: "Frigid") case .hail: - return NSLocalizedString("WeatherCondition.hail", bundle: Bundle.main, comment: "Hail") + return NSLocalizedString("WeatherCondition.hail", bundle: Bundle.module, comment: "Hail") case .haze: - return NSLocalizedString("WeatherCondition.haze", bundle: Bundle.main, comment: "Haze") + return NSLocalizedString("WeatherCondition.haze", bundle: Bundle.module, comment: "Haze") case .heavyRain: - return NSLocalizedString("WeatherCondition.heavyRain", bundle: Bundle.main, comment: "Heavy Rain") + return NSLocalizedString("WeatherCondition.heavyRain", bundle: Bundle.module, comment: "Heavy Rain") case .heavySnow: - return NSLocalizedString("WeatherCondition.heavySnow", bundle: Bundle.main, comment: "Heavy Snow") + return NSLocalizedString("WeatherCondition.heavySnow", bundle: Bundle.module, comment: "Heavy Snow") case .hot: - return NSLocalizedString("WeatherCondition.hot", bundle: Bundle.main, comment: "Hot") + return NSLocalizedString("WeatherCondition.hot", bundle: Bundle.module, comment: "Hot") case .hurricane: - return NSLocalizedString("WeatherCondition.hurricane", bundle: Bundle.main, comment: "Hurricane") + return NSLocalizedString("WeatherCondition.hurricane", bundle: Bundle.module, comment: "Hurricane") case .isolatedThunderstorms: - return NSLocalizedString("WeatherCondition.isolatedThunderstorms", bundle: Bundle.main, comment: "Isolated Thunderstorms") + return NSLocalizedString("WeatherCondition.isolatedThunderstorms", bundle: Bundle.module, comment: "Isolated Thunderstorms") case .mostlyClear: - return NSLocalizedString("WeatherCondition.mostlyClear", bundle: Bundle.main, comment: "Mostly Clear") + return NSLocalizedString("WeatherCondition.mostlyClear", bundle: Bundle.module, comment: "Mostly Clear") case .mostlyCloudy: - return NSLocalizedString("WeatherCondition.mostlyCloudy", bundle: Bundle.main, comment: "Mostly Cloudy") + return NSLocalizedString("WeatherCondition.mostlyCloudy", bundle: Bundle.module, comment: "Mostly Cloudy") case .partlyCloudy: - return NSLocalizedString("WeatherCondition.partlyCloudy", bundle: Bundle.main, comment: "Partly Cloudy") + return NSLocalizedString("WeatherCondition.partlyCloudy", bundle: Bundle.module, comment: "Partly Cloudy") case .rain: - return NSLocalizedString("WeatherCondition.rain", bundle: Bundle.main, comment: "Rain") + return NSLocalizedString("WeatherCondition.rain", bundle: Bundle.module, comment: "Rain") case .scatteredThunderstorms: - return NSLocalizedString("WeatherCondition.scatteredThunderstorms", bundle: Bundle.main, comment: "Scattered Thunderstorms") + return NSLocalizedString("WeatherCondition.scatteredThunderstorms", bundle: Bundle.module, comment: "Scattered Thunderstorms") case .sleet: - return NSLocalizedString("WeatherCondition.sleet", bundle: Bundle.main, comment: "Sleet") + return NSLocalizedString("WeatherCondition.sleet", bundle: Bundle.module, comment: "Sleet") case .smoky: - return NSLocalizedString("WeatherCondition.smoky", bundle: Bundle.main, comment: "Smoky") + return NSLocalizedString("WeatherCondition.smoky", bundle: Bundle.module, comment: "Smoky") case .snow: - return NSLocalizedString("WeatherCondition.snow", bundle: Bundle.main, comment: "Snow") + return NSLocalizedString("WeatherCondition.snow", bundle: Bundle.module, comment: "Snow") case .strongStorms: - return NSLocalizedString("WeatherCondition.strongStorms", bundle: Bundle.main, comment: "StrongStorms") + return NSLocalizedString("WeatherCondition.strongStorms", bundle: Bundle.module, comment: "StrongStorms") case .sunFlurries: - return NSLocalizedString("WeatherCondition.sunFlurries", bundle: Bundle.main, comment: "Sun Flurries") + return NSLocalizedString("WeatherCondition.sunFlurries", bundle: Bundle.module, comment: "Sun Flurries") case .sunShowers: - return NSLocalizedString("WeatherCondition.sunShowers", bundle: Bundle.main, comment: "Sun Showers") + return NSLocalizedString("WeatherCondition.sunShowers", bundle: Bundle.module, comment: "Sun Showers") case .thunderstorms: - return NSLocalizedString("WeatherCondition.thunderstorms", bundle: Bundle.main, comment: "Thunderstorms") + return NSLocalizedString("WeatherCondition.thunderstorms", bundle: Bundle.module, comment: "Thunderstorms") case .tropicalStorm: - return NSLocalizedString("WeatherCondition.tropicalStorm", bundle: Bundle.main, comment: "Tropical Storm") + return NSLocalizedString("WeatherCondition.tropicalStorm", bundle: Bundle.module, comment: "Tropical Storm") case .undefined: - return NSLocalizedString("WeatherCondition.undefined", bundle: Bundle.main, comment: "undefined") + return NSLocalizedString("WeatherCondition.undefined", bundle: Bundle.module, comment: "undefined") case .windy: - return NSLocalizedString("WeatherCondition.windy", bundle: Bundle.main, comment: "Windy") + return NSLocalizedString("WeatherCondition.windy", bundle: Bundle.module, comment: "Windy") case .wintryMix: - return NSLocalizedString("WeatherCondition.wintryMix", bundle: Bundle.main, comment: "Wintry Mix") + return NSLocalizedString("WeatherCondition.wintryMix", bundle: Bundle.module, comment: "Wintry Mix") } } @@ -163,75 +163,75 @@ public enum WeatherCondition : String, CaseIterable, CustomStringConvertible, Ha public var accessibilityDescription: String { switch self { case .blizzard: - return NSLocalizedString("WeatherCondition.accessibility.blizzard", bundle: Bundle.main, comment: "Blizzard") + return NSLocalizedString("WeatherCondition.accessibility.blizzard", bundle: Bundle.module, comment: "Blizzard") case .blowingDust: - return NSLocalizedString("WeatherCondition.accessibility.blowingDust", bundle: Bundle.main, comment: "Blowing Dust") + return NSLocalizedString("WeatherCondition.accessibility.blowingDust", bundle: Bundle.module, comment: "Blowing Dust") case .blowingSnow: - return NSLocalizedString("WeatherCondition.accessibility.blowingSnow", bundle: Bundle.main, comment: "Blowing Snow") + return NSLocalizedString("WeatherCondition.accessibility.blowingSnow", bundle: Bundle.module, comment: "Blowing Snow") case .breezy: - return NSLocalizedString("WeatherCondition.accessibility.breezy", bundle: Bundle.main, comment: "Breezy") + return NSLocalizedString("WeatherCondition.accessibility.breezy", bundle: Bundle.module, comment: "Breezy") case .clear: - return NSLocalizedString("WeatherCondition.accessibility.clear", bundle: Bundle.main, comment: "Clear") + return NSLocalizedString("WeatherCondition.accessibility.clear", bundle: Bundle.module, comment: "Clear") case .cloudy: - return NSLocalizedString("WeatherCondition.accessibility.cloudy", bundle: Bundle.main, comment: "Cloudy") + return NSLocalizedString("WeatherCondition.accessibility.cloudy", bundle: Bundle.module, comment: "Cloudy") case .drizzle: - return NSLocalizedString("WeatherCondition.accessibility.drizzle", bundle: Bundle.main, comment: "Drizzle") + return NSLocalizedString("WeatherCondition.accessibility.drizzle", bundle: Bundle.module, comment: "Drizzle") case .flurries: - return NSLocalizedString("WeatherCondition.accessibility.flurries", bundle: Bundle.main, comment: "Flurries") + return NSLocalizedString("WeatherCondition.accessibility.flurries", bundle: Bundle.module, comment: "Flurries") case .foggy: - return NSLocalizedString("WeatherCondition.accessibility.foggy", bundle: Bundle.main, comment: "Foggy") + return NSLocalizedString("WeatherCondition.accessibility.foggy", bundle: Bundle.module, comment: "Foggy") case .freezingDrizzle: - return NSLocalizedString("WeatherCondition.accessibility.freezingDrizzle", bundle: Bundle.main, comment: "Freezing Drizzle") + return NSLocalizedString("WeatherCondition.accessibility.freezingDrizzle", bundle: Bundle.module, comment: "Freezing Drizzle") case .freezingRain: - return NSLocalizedString("WeatherCondition.accessibility.freezingRain", bundle: Bundle.main, comment: "Freezing Rain") + return NSLocalizedString("WeatherCondition.accessibility.freezingRain", bundle: Bundle.module, comment: "Freezing Rain") case .frigid: - return NSLocalizedString("WeatherCondition.accessibility.frigid", bundle: Bundle.main, comment: "Frigid") + return NSLocalizedString("WeatherCondition.accessibility.frigid", bundle: Bundle.module, comment: "Frigid") case .hail: - return NSLocalizedString("WeatherCondition.accessibility.hail", bundle: Bundle.main, comment: "Hail") + return NSLocalizedString("WeatherCondition.accessibility.hail", bundle: Bundle.module, comment: "Hail") case .haze: - return NSLocalizedString("WeatherCondition.accessibility.haze", bundle: Bundle.main, comment: "Haze") + return NSLocalizedString("WeatherCondition.accessibility.haze", bundle: Bundle.module, comment: "Haze") case .heavyRain: - return NSLocalizedString("WeatherCondition.accessibility.heavyRain", bundle: Bundle.main, comment: "Heavy Rain") + return NSLocalizedString("WeatherCondition.accessibility.heavyRain", bundle: Bundle.module, comment: "Heavy Rain") case .heavySnow: - return NSLocalizedString("WeatherCondition.accessibility.heavySnow", bundle: Bundle.main, comment: "Heavy Snow") + return NSLocalizedString("WeatherCondition.accessibility.heavySnow", bundle: Bundle.module, comment: "Heavy Snow") case .hot: - return NSLocalizedString("WeatherCondition.accessibility.hot", bundle: Bundle.main, comment: "Hot") + return NSLocalizedString("WeatherCondition.accessibility.hot", bundle: Bundle.module, comment: "Hot") case .hurricane: - return NSLocalizedString("WeatherCondition.accessibility.hurricane", bundle: Bundle.main, comment: "Hurricane") + return NSLocalizedString("WeatherCondition.accessibility.hurricane", bundle: Bundle.module, comment: "Hurricane") case .isolatedThunderstorms: - return NSLocalizedString("WeatherCondition.accessibility.isolatedThunderstorms", bundle: Bundle.main, comment: "Isolated Thunderstorms") + return NSLocalizedString("WeatherCondition.accessibility.isolatedThunderstorms", bundle: Bundle.module, comment: "Isolated Thunderstorms") case .mostlyClear: - return NSLocalizedString("WeatherCondition.accessibility.mostlyClear", bundle: Bundle.main, comment: "Mostly Clear") + return NSLocalizedString("WeatherCondition.accessibility.mostlyClear", bundle: Bundle.module, comment: "Mostly Clear") case .mostlyCloudy: - return NSLocalizedString("WeatherCondition.accessibility.mostlyCloudy", bundle: Bundle.main, comment: "Mostly Cloudy") + return NSLocalizedString("WeatherCondition.accessibility.mostlyCloudy", bundle: Bundle.module, comment: "Mostly Cloudy") case .partlyCloudy: - return NSLocalizedString("WeatherCondition.accessibility.partlyCloudy", bundle: Bundle.main, comment: "Partly Cloudy") + return NSLocalizedString("WeatherCondition.accessibility.partlyCloudy", bundle: Bundle.module, comment: "Partly Cloudy") case .rain: - return NSLocalizedString("WeatherCondition.accessibility.rain", bundle: Bundle.main, comment: "Rain") + return NSLocalizedString("WeatherCondition.accessibility.rain", bundle: Bundle.module, comment: "Rain") case .scatteredThunderstorms: - return NSLocalizedString("WeatherCondition.accessibility.scatteredThunderstorms", bundle: Bundle.main, comment: "Scattered Thunderstorms") + return NSLocalizedString("WeatherCondition.accessibility.scatteredThunderstorms", bundle: Bundle.module, comment: "Scattered Thunderstorms") case .sleet: - return NSLocalizedString("WeatherCondition.accessibility.sleet", bundle: Bundle.main, comment: "Sleet") + return NSLocalizedString("WeatherCondition.accessibility.sleet", bundle: Bundle.module, comment: "Sleet") case .smoky: - return NSLocalizedString("WeatherCondition.accessibility.smoky", bundle: Bundle.main, comment: "Smoky") + return NSLocalizedString("WeatherCondition.accessibility.smoky", bundle: Bundle.module, comment: "Smoky") case .snow: - return NSLocalizedString("WeatherCondition.accessibility.snow", bundle: Bundle.main, comment: "Snow") + return NSLocalizedString("WeatherCondition.accessibility.snow", bundle: Bundle.module, comment: "Snow") case .strongStorms: - return NSLocalizedString("WeatherCondition.accessibility.strongStorms", bundle: Bundle.main, comment: "StrongStorms") + return NSLocalizedString("WeatherCondition.accessibility.strongStorms", bundle: Bundle.module, comment: "StrongStorms") case .sunFlurries: - return NSLocalizedString("WeatherCondition.accessibility.sunFlurries", bundle: Bundle.main, comment: "Sun Flurries") + return NSLocalizedString("WeatherCondition.accessibility.sunFlurries", bundle: Bundle.module, comment: "Sun Flurries") case .sunShowers: - return NSLocalizedString("WeatherCondition.accessibility.sunShowers", bundle: Bundle.main, comment: "Sun Showers") + return NSLocalizedString("WeatherCondition.accessibility.sunShowers", bundle: Bundle.module, comment: "Sun Showers") case .thunderstorms: - return NSLocalizedString("WeatherCondition.accessibility.thunderstorms", bundle: Bundle.main, comment: "Thunderstorms") + return NSLocalizedString("WeatherCondition.accessibility.thunderstorms", bundle: Bundle.module, comment: "Thunderstorms") case .tropicalStorm: - return NSLocalizedString("WeatherCondition.accessibility.tropicalStorm", bundle: Bundle.main, comment: "Tropical Storm") + return NSLocalizedString("WeatherCondition.accessibility.tropicalStorm", bundle: Bundle.module, comment: "Tropical Storm") case .undefined: - return NSLocalizedString("WeatherCondition.accessibility.undefined", bundle: Bundle.main, comment: "undefined") + return NSLocalizedString("WeatherCondition.accessibility.undefined", bundle: Bundle.module, comment: "undefined") case .windy: - return NSLocalizedString("WeatherCondition.accessibility.windy", bundle: Bundle.main, comment: "Windy") + return NSLocalizedString("WeatherCondition.accessibility.windy", bundle: Bundle.module, comment: "Windy") case .wintryMix: - return NSLocalizedString("WeatherCondition.accessibility.wintryMix", bundle: Bundle.main, comment: "Wintry Mix") + return NSLocalizedString("WeatherCondition.accessibility.wintryMix", bundle: Bundle.module, comment: "Wintry Mix") } } } diff --git a/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift b/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift index 5bc71ff..3cbf672 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift @@ -30,15 +30,15 @@ public enum WeatherSeverity : String, Codable, CaseIterable, CustomStringConvert public var description: String { switch self { case .minor: - return NSLocalizedString("WeatherSeverity.minor", bundle: Bundle.main, comment: "Minor") + return NSLocalizedString("WeatherSeverity.minor", bundle: Bundle.module, comment: "Minor") case .moderate: - return NSLocalizedString("WeatherSeverity.moderate", bundle: Bundle.main, comment: "Moderate") + return NSLocalizedString("WeatherSeverity.moderate", bundle: Bundle.module, comment: "Moderate") case .severe: - return NSLocalizedString("WeatherSeverity.severe", bundle: Bundle.main, comment: "Severe") + return NSLocalizedString("WeatherSeverity.severe", bundle: Bundle.module, comment: "Severe") case .extreme: - return NSLocalizedString("WeatherSeverity.extreme", bundle: Bundle.main, comment: "Extreme") + return NSLocalizedString("WeatherSeverity.extreme", bundle: Bundle.module, comment: "Extreme") case .unknown: - return NSLocalizedString("WeatherSeverity.unknown", bundle: Bundle.main, comment: "Unknown") + return NSLocalizedString("WeatherSeverity.unknown", bundle: Bundle.module, comment: "Unknown") } } @@ -47,15 +47,15 @@ public enum WeatherSeverity : String, Codable, CaseIterable, CustomStringConvert public var accessibilityDescription: String { switch self { case .minor: - return NSLocalizedString("WeatherSeverity.accessibility.minor", bundle: Bundle.main, comment: "Minor") + return NSLocalizedString("WeatherSeverity.accessibility.minor", bundle: Bundle.module, comment: "Minor") case .moderate: - return NSLocalizedString("WeatherSeverity.accessibility.moderate", bundle: Bundle.main, comment: "Moderate") + return NSLocalizedString("WeatherSeverity.accessibility.moderate", bundle: Bundle.module, comment: "Moderate") case .severe: - return NSLocalizedString("WeatherSeverity.accessibility.severe", bundle: Bundle.main, comment: "Severe") + return NSLocalizedString("WeatherSeverity.accessibility.severe", bundle: Bundle.module, comment: "Severe") case .extreme: - return NSLocalizedString("WeatherSeverity.accessibility.extreme", bundle: Bundle.main, comment: "Extreme") + return NSLocalizedString("WeatherSeverity.accessibility.extreme", bundle: Bundle.module, comment: "Extreme") case .unknown: - return NSLocalizedString("WeatherSeverity.accessibility.unknown", bundle: Bundle.main, comment: "Unknown") + return NSLocalizedString("WeatherSeverity.accessibility.unknown", bundle: Bundle.module, comment: "Unknown") } } } diff --git a/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift b/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift index 785229c..4204091 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift @@ -70,39 +70,39 @@ public struct Wind: Sendable { public var abbreviation: String { switch self { case .north: - return NSLocalizedString("Wind.abbreviation.north", bundle: Bundle.main, comment: "N") + return NSLocalizedString("Wind.abbreviation.north", bundle: Bundle.module, comment: "N") case .northNortheast: - return NSLocalizedString("Wind.abbreviation.northNortheast", bundle: Bundle.main, comment: "NNE") + return NSLocalizedString("Wind.abbreviation.northNortheast", bundle: Bundle.module, comment: "NNE") case .northeast: - return NSLocalizedString("Wind.abbreviation.northeast", bundle: Bundle.main, comment: "NE") + return NSLocalizedString("Wind.abbreviation.northeast", bundle: Bundle.module, comment: "NE") case .eastNortheast: - return NSLocalizedString("Wind.abbreviation.eastNortheast", bundle: Bundle.main, comment: "ENE") + return NSLocalizedString("Wind.abbreviation.eastNortheast", bundle: Bundle.module, comment: "ENE") case .east: - return NSLocalizedString("Wind.abbreviation.east", bundle: Bundle.main, comment: "E") + return NSLocalizedString("Wind.abbreviation.east", bundle: Bundle.module, comment: "E") case .eastSoutheast: - return NSLocalizedString("Wind.abbreviation.eastSoutheast", bundle: Bundle.main, comment: "ESE") + return NSLocalizedString("Wind.abbreviation.eastSoutheast", bundle: Bundle.module, comment: "ESE") case .southeast: - return NSLocalizedString("Wind.abbreviation.southeast", bundle: Bundle.main, comment: "SE") + return NSLocalizedString("Wind.abbreviation.southeast", bundle: Bundle.module, comment: "SE") case .southSoutheast: - return NSLocalizedString("Wind.abbreviation.southSoutheast", bundle: Bundle.main, comment: "SSE") + return NSLocalizedString("Wind.abbreviation.southSoutheast", bundle: Bundle.module, comment: "SSE") case .south: - return NSLocalizedString("Wind.abbreviation.south", bundle: Bundle.main, comment: "S") + return NSLocalizedString("Wind.abbreviation.south", bundle: Bundle.module, comment: "S") case .southSouthwest: - return NSLocalizedString("Wind.abbreviation.southSouthwest", bundle: Bundle.main, comment: "SSW") + return NSLocalizedString("Wind.abbreviation.southSouthwest", bundle: Bundle.module, comment: "SSW") case .southwest: - return NSLocalizedString("Wind.abbreviation.southwest", bundle: Bundle.main, comment: "SW") + return NSLocalizedString("Wind.abbreviation.southwest", bundle: Bundle.module, comment: "SW") case .westSouthwest: - return NSLocalizedString("Wind.abbreviation.westSouthwest", bundle: Bundle.main, comment: "WSW") + return NSLocalizedString("Wind.abbreviation.westSouthwest", bundle: Bundle.module, comment: "WSW") case .west: - return NSLocalizedString("Wind.abbreviation.west", bundle: Bundle.main, comment: "W") + return NSLocalizedString("Wind.abbreviation.west", bundle: Bundle.module, comment: "W") case .westNorthwest: - return NSLocalizedString("Wind.abbreviation.westNorthwest", bundle: Bundle.main, comment: "WNW") + return NSLocalizedString("Wind.abbreviation.westNorthwest", bundle: Bundle.module, comment: "WNW") case .northwest: - return NSLocalizedString("Wind.abbreviation.northwest", bundle: Bundle.main, comment: "NW") + return NSLocalizedString("Wind.abbreviation.northwest", bundle: Bundle.module, comment: "NW") case .northNorthwest: - return NSLocalizedString("Wind.abbreviation.northNorthwest", bundle: Bundle.main, comment: "NNW") + return NSLocalizedString("Wind.abbreviation.northNorthwest", bundle: Bundle.module, comment: "NNW") case .undefined: - return NSLocalizedString("Wind.abbreviation.undefined", bundle: Bundle.main, comment: "Undefined") + return NSLocalizedString("Wind.abbreviation.undefined", bundle: Bundle.module, comment: "Undefined") } } @@ -111,39 +111,39 @@ public struct Wind: Sendable { public var description: String { switch self { case .north: - return NSLocalizedString("Wind.north", bundle: Bundle.main, comment: "North") + return NSLocalizedString("Wind.north", bundle: Bundle.module, comment: "North") case .northNortheast: - return NSLocalizedString("Wind.northNortheast", bundle: Bundle.main, comment: "North Northeast") + return NSLocalizedString("Wind.northNortheast", bundle: Bundle.module, comment: "North Northeast") case .northeast: - return NSLocalizedString("Wind.northeast", bundle: Bundle.main, comment: "Northeast") + return NSLocalizedString("Wind.northeast", bundle: Bundle.module, comment: "Northeast") case .eastNortheast: - return NSLocalizedString("Wind.eastNortheast", bundle: Bundle.main, comment: "East Northeast") + return NSLocalizedString("Wind.eastNortheast", bundle: Bundle.module, comment: "East Northeast") case .east: - return NSLocalizedString("Wind.east", bundle: Bundle.main, comment: "East") + return NSLocalizedString("Wind.east", bundle: Bundle.module, comment: "East") case .eastSoutheast: - return NSLocalizedString("Wind.eastSoutheast", bundle: Bundle.main, comment: "East Souteast") + return NSLocalizedString("Wind.eastSoutheast", bundle: Bundle.module, comment: "East Souteast") case .southeast: - return NSLocalizedString("Wind.southeast", bundle: Bundle.main, comment: "Southeast") + return NSLocalizedString("Wind.southeast", bundle: Bundle.module, comment: "Southeast") case .southSoutheast: - return NSLocalizedString("Wind.southSoutheast", bundle: Bundle.main, comment: "South") + return NSLocalizedString("Wind.southSoutheast", bundle: Bundle.module, comment: "South") case .south: - return NSLocalizedString("Wind.south", bundle: Bundle.main, comment: "South Southeast") + return NSLocalizedString("Wind.south", bundle: Bundle.module, comment: "South Southeast") case .southSouthwest: - return NSLocalizedString("Wind.southSouthwest", bundle: Bundle.main, comment: "South Southwest") + return NSLocalizedString("Wind.southSouthwest", bundle: Bundle.module, comment: "South Southwest") case .southwest: - return NSLocalizedString("Wind.southwest", bundle: Bundle.main, comment: "Southwest") + return NSLocalizedString("Wind.southwest", bundle: Bundle.module, comment: "Southwest") case .westSouthwest: - return NSLocalizedString("Wind.westSouthwest", bundle: Bundle.main, comment: "West Southwest") + return NSLocalizedString("Wind.westSouthwest", bundle: Bundle.module, comment: "West Southwest") case .west: - return NSLocalizedString("Wind.west", bundle: Bundle.main, comment: "West") + return NSLocalizedString("Wind.west", bundle: Bundle.module, comment: "West") case .westNorthwest: - return NSLocalizedString("Wind.westNorthwest", bundle: Bundle.main, comment: "West Northwest") + return NSLocalizedString("Wind.westNorthwest", bundle: Bundle.module, comment: "West Northwest") case .northwest: - return NSLocalizedString("Wind.northwest", bundle: Bundle.main, comment: "Northwest") + return NSLocalizedString("Wind.northwest", bundle: Bundle.module, comment: "Northwest") case .northNorthwest: - return NSLocalizedString("Wind.northNorthwest", bundle: Bundle.main, comment: "North Northwest") + return NSLocalizedString("Wind.northNorthwest", bundle: Bundle.module, comment: "North Northwest") case .undefined: - return NSLocalizedString("Wind.undefined", bundle: Bundle.main, comment: "Undefined") + return NSLocalizedString("Wind.undefined", bundle: Bundle.module, comment: "Undefined") } } @@ -154,39 +154,39 @@ public struct Wind: Sendable { public var accessibilityDescription: String { switch self { case .north: - return NSLocalizedString("Wind.accessibility.north", bundle: Bundle.main, comment: "North") + return NSLocalizedString("Wind.accessibility.north", bundle: Bundle.module, comment: "North") case .northNortheast: - return NSLocalizedString("Wind.accessibility.northNortheast", bundle: Bundle.main, comment: "North Northeast") + return NSLocalizedString("Wind.accessibility.northNortheast", bundle: Bundle.module, comment: "North Northeast") case .northeast: - return NSLocalizedString("Wind.accessibility.northeast", bundle: Bundle.main, comment: "Northeast") + return NSLocalizedString("Wind.accessibility.northeast", bundle: Bundle.module, comment: "Northeast") case .eastNortheast: - return NSLocalizedString("Wind.accessibility.eastNortheast", bundle: Bundle.main, comment: "East Northeast") + return NSLocalizedString("Wind.accessibility.eastNortheast", bundle: Bundle.module, comment: "East Northeast") case .east: - return NSLocalizedString("Wind.accessibility.east", bundle: Bundle.main, comment: "East") + return NSLocalizedString("Wind.accessibility.east", bundle: Bundle.module, comment: "East") case .eastSoutheast: - return NSLocalizedString("Wind.accessibility.eastSoutheast", bundle: Bundle.main, comment: "East Souteast") + return NSLocalizedString("Wind.accessibility.eastSoutheast", bundle: Bundle.module, comment: "East Souteast") case .southeast: - return NSLocalizedString("Wind.accessibility.southeast", bundle: Bundle.main, comment: "Southeast") + return NSLocalizedString("Wind.accessibility.southeast", bundle: Bundle.module, comment: "Southeast") case .southSoutheast: - return NSLocalizedString("Wind.accessibility.southSoutheast", bundle: Bundle.main, comment: "South") + return NSLocalizedString("Wind.accessibility.southSoutheast", bundle: Bundle.module, comment: "South") case .south: - return NSLocalizedString("Wind.accessibility.south", bundle: Bundle.main, comment: "South Southeast") + return NSLocalizedString("Wind.accessibility.south", bundle: Bundle.module, comment: "South Southeast") case .southSouthwest: - return NSLocalizedString("Wind.accessibility.southSouthwest", bundle: Bundle.main, comment: "South Southwest") + return NSLocalizedString("Wind.accessibility.southSouthwest", bundle: Bundle.module, comment: "South Southwest") case .southwest: - return NSLocalizedString("Wind.accessibility.southwest", bundle: Bundle.main, comment: "Southwest") + return NSLocalizedString("Wind.accessibility.southwest", bundle: Bundle.module, comment: "Southwest") case .westSouthwest: - return NSLocalizedString("Wind.accessibility.westSouthwest", bundle: Bundle.main, comment: "West Southwest") + return NSLocalizedString("Wind.accessibility.westSouthwest", bundle: Bundle.module, comment: "West Southwest") case .west: - return NSLocalizedString("Wind.accessibility.west", bundle: Bundle.main, comment: "West") + return NSLocalizedString("Wind.accessibility.west", bundle: Bundle.module, comment: "West") case .westNorthwest: - return NSLocalizedString("Wind.accessibility.westNorthwest", bundle: Bundle.main, comment: "West Northwest") + return NSLocalizedString("Wind.accessibility.westNorthwest", bundle: Bundle.module, comment: "West Northwest") case .northwest: - return NSLocalizedString("Wind.accessibility.northwest", bundle: Bundle.main, comment: "Northwest") + return NSLocalizedString("Wind.accessibility.northwest", bundle: Bundle.module, comment: "Northwest") case .northNorthwest: - return NSLocalizedString("Wind.accessibility.northNorthwest", bundle: Bundle.main, comment: "North Northwest") + return NSLocalizedString("Wind.accessibility.northNorthwest", bundle: Bundle.module, comment: "North Northwest") case .undefined: - return NSLocalizedString("Wind.accessibility.undefined", bundle: Bundle.main, comment: "Undefined") + return NSLocalizedString("Wind.accessibility.undefined", bundle: Bundle.module, comment: "Undefined") } } diff --git a/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift b/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift index 9f1e454..8d16deb 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift @@ -23,20 +23,37 @@ public struct DayWeather: Sendable { /// The daytime high temperature. public var highTemperature: Measurement + /// The time at which the high temperature occurs on this day. + public var highTemperatureTime: Date? + /// The overnight low temperature. public var lowTemperature: Measurement + /// The time at which the low temperature occurs on this day. + public var lowTemperatureTime: Date? + + /// The maximum amount of water vapor in the air for the day. + /// + /// Relative humidity measures the amount of water vapor in the air, compared to the maximum amount that the air can hold at the current temperature. + /// + /// The range of this property is from `0` to `1`, inclusive. + public var maximumHumidity: Double + + /// The minimum amount of water vapor in the air for the day. + /// + /// Relative humidity measures the amount of water vapor in the air, compared to the maximum amount that the air can hold at the current temperature. + /// + /// The range of this property is from `0` to `1`, inclusive. + public var minimumHumidity: Double + /// Description of precipitation for this day. public var precipitation: Precipitation /// The probability of precipitation during the day, from 0 to 1. public var precipitationChance: Double - /// The amount of rainfall for the day. - public var rainfallAmount: Measurement - - /// The amount of snowfall for the day. - public var snowfallAmount: Measurement + /// A breakdown of amounts of all forms of precipitation forecasted for the day. + public var precipitationAmountByType: PrecipitationAmountByType /// The solar events for the day. public var sun: SunEvents @@ -47,8 +64,80 @@ public struct DayWeather: Sendable { /// The UV index provides the expected intensity of ultraviolet radiation from the sun. public var uvIndex: UVIndex + /// The maximum distance at which terrain is visible for the day. + /// + /// The amount of light, and weather conditions like fog, mist, and smog affect visibility. + public var maximumVisibility: Double + + /// The minimum distance at which terrain is visible for the day. + /// + /// The amount of light, and weather conditions like fog, mist, and smog affect visibility. + public var minimumVisibility: Double + /// Contains wind data of speed, bearing (direction), gust. public var wind: Wind + + /// The maximum sustained wind speed. + public var highWindSpeed: Measurement? + + /// The weather forecast from 7AM - 7PM on this day. + public var daytimeForecast: DayPartForecast + + /// The weather forecast for 7PM on this day until 7AM the following day. + public var overnightForecast: DayPartForecast + + /// The forecast from now until midnight local time. + /// + /// The value is only available for the current day. + public var restOfDayForecast: DayPartForecast? + + internal init( + date: Date, + condition: WeatherCondition, + symbolName: String, + highTemperature: Measurement, + highTemperatureTime: Date?, + lowTemperature: Measurement, + lowTemperatureTime: Date?, + maximumHumidity: Double, + minimumHumidity: Double, + precipitation: Precipitation, + precipitationChance: Double, + precipitationAmountByType: PrecipitationAmountByType, + sun: SunEvents, + moon: MoonEvents, + uvIndex: UVIndex, + maximumVisibility: Double, + minimumVisibility: Double, + wind: Wind, + highWindSpeed: Measurement?, + daytimeForecast: DayPartForecast, + overnightForecast: DayPartForecast, + restOfDayForecast: DayPartForecast? + ) { + self.date = date + self.condition = condition + self.symbolName = symbolName + self.highTemperature = highTemperature + self.highTemperatureTime = highTemperatureTime + self.lowTemperature = lowTemperature + self.lowTemperatureTime = lowTemperatureTime + self.maximumHumidity = maximumHumidity + self.minimumHumidity = minimumHumidity + self.precipitation = precipitation + self.precipitationChance = precipitationChance + self.precipitationAmountByType = precipitationAmountByType + self.sun = sun + self.moon = moon + self.uvIndex = uvIndex + self.maximumVisibility = maximumVisibility + self.minimumVisibility = minimumVisibility + self.wind = wind + self.highWindSpeed = highWindSpeed + self.daytimeForecast = daytimeForecast + self.overnightForecast = overnightForecast + self.restOfDayForecast = restOfDayForecast + } } extension DayWeather: Codable {} diff --git a/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift b/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift index 373a248..5bf2daf 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift @@ -18,6 +18,9 @@ public struct HourWeather: Sendable { /// sky obscured by clouds when observed from a given location. public var cloudCover: Double + /// The percentage of the sky covered with low altitude, middle altitude and high altitude clouds during the period. + public var cloudCoverByAltitude: CloudCoverByAltitude + /// A description of the weather condition for this hour. public var condition: WeatherCondition @@ -46,6 +49,9 @@ public struct HourWeather: Sendable { /// precipitation amounts. public var precipitationAmount: Measurement + /// The rate at which snow crystals are falling, in millimeters per hour. + public var precipitationIntensity: Measurement + /// The sea level pressure, which describes the atmospheric pressure at sea level at a given location. /// It is a reduced pressure calculated by using observed conditions to remove the effects of elevation /// from pressure readings. @@ -54,10 +60,10 @@ public struct HourWeather: Sendable { /// The pressure trend, or barometric tendency, is the kind and amount of atmospheric pressure /// change over time. public var pressureTrend: PressureTrend - + /// The rate at which snow crystals are falling, in millimeters per hour. public var snowfallIntensity: Measurement - + /// The amount of snowfall for the hour. public var snowfallAmount: Measurement diff --git a/Sources/OpenWeatherKit/Public/WeatherError.swift b/Sources/OpenWeatherKit/Public/WeatherError.swift index 0f79dbc..9593b6e 100644 --- a/Sources/OpenWeatherKit/Public/WeatherError.swift +++ b/Sources/OpenWeatherKit/Public/WeatherError.swift @@ -1,6 +1,6 @@ // // WeatherError.swift -// +// // // Created by Jeremy Greenwood on 8/28/22. // @@ -19,31 +19,38 @@ public enum WeatherError : LocalizedError, Equatable, Hashable { /// An unknown error. case unknown + /// Could not find timezone + case timezone + case missingData(_ attributeName: String) /// A localized message describing what error occurred. public var errorDescription: String? { switch self { - case .countryCode: return NSLocalizedString("Error.countryCode", bundle: Bundle.main, comment: "Could not determine country code") + case .countryCode: return NSLocalizedString("Error.countryCode", bundle: Bundle.module, comment: "Could not determine country code") case .permissionDenied: - return NSLocalizedString("Error.permissionDenied", bundle: Bundle.main, comment: "Permission Denied") + return NSLocalizedString("Error.permissionDenied", bundle: Bundle.module, comment: "Permission Denied") case .unknown: - return NSLocalizedString("Error.unknown", bundle: Bundle.main, comment: "Unknown Error") + return NSLocalizedString("Error.unknown", bundle: Bundle.module, comment: "Unknown Error") + case .timezone: + return NSLocalizedString("Error.timezone", bundle: Bundle.module, comment: "Could not determine timezone") case let .missingData(name): - return String(format: NSLocalizedString("Error.missingData", bundle: Bundle.main, comment: "The data \(name) is missing from the response"), name) + return String(format: NSLocalizedString("Error.missingData", bundle: Bundle.module, comment: "The data \(name) is missing from the response"), name) } } /// A localized message describing the reason for the failure. public var failureReason: String? { switch self { - case .countryCode: return NSLocalizedString("Error.countryCode", bundle: Bundle.main, comment: "Could not determine country code") + case .countryCode: return NSLocalizedString("Error.countryCode", bundle: Bundle.module, comment: "Could not determine country code") case .permissionDenied: - return NSLocalizedString("Error.permissionDenied", bundle: Bundle.main, comment: "Permission Denied") + return NSLocalizedString("Error.permissionDenied", bundle: Bundle.module, comment: "Permission Denied") case .unknown: - return NSLocalizedString("Error.unknown", bundle: Bundle.main, comment: "Unknown Error") + return NSLocalizedString("Error.unknown", bundle: Bundle.module, comment: "Unknown Error") + case .timezone: + return NSLocalizedString("Error.timezone", bundle: Bundle.module, comment: "Could not determine timezone") case let .missingData(name): - return String(format: NSLocalizedString("Error.missingData", bundle: Bundle.main, comment: "The data \(name) is missing from the response"), name) + return String(format: NSLocalizedString("Error.missingData", bundle: Bundle.module, comment: "The data \(name) is missing from the response"), name) } } diff --git a/Sources/OpenWeatherKit/Public/WeatherService.swift b/Sources/OpenWeatherKit/Public/WeatherService.swift index bf213b1..0adfd34 100644 --- a/Sources/OpenWeatherKit/Public/WeatherService.swift +++ b/Sources/OpenWeatherKit/Public/WeatherService.swift @@ -1,6 +1,6 @@ // // WeatherService.swift -// +// // // Created by Jeremy Greenwood on 8/28/22. // @@ -21,7 +21,7 @@ final public class WeatherService: Sendable { /// Establishes the configuration for weather requests. @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Configuration: Sendable { - + public enum Language: String, Sendable { case englishUS = "en_US" case germanDE = "de_DE" @@ -115,8 +115,8 @@ final public class WeatherService: Sendable { /// @inlinable final public func weather(for location: LocationProtocol) async throws -> Weather { - guard let countryCode = try await geocoder.countryCode(location) else { throw WeatherError.countryCode } - return try await getWeather(location: location, countryCode: countryCode) + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await getWeather(location: location, countryCode: countryCode, timezone: timezone) } #endif @@ -127,8 +127,13 @@ final public class WeatherService: Sendable { /// - Returns: The aggregate weather. /// @inlinable - final public func weather(for location: LocationProtocol, countryCode: String, language: WeatherService.Configuration.Language? = nil) async throws -> Weather { - try await getWeather(location: location, countryCode: countryCode, language: language) + final public func weather( + for location: LocationProtocol, + countryCode: String, + timezone: TimeZone, + language: WeatherService.Configuration.Language? = nil + ) async throws -> Weather { + try await getWeather(location: location, countryCode: countryCode, timezone: timezone, language: language) } /// @@ -144,23 +149,31 @@ final public class WeatherService: Sendable { /// Example usage: /// `let current = try await service.weather(for: newYork, including: .current)` /// +#if canImport(CoreLocation) @inlinable final public func weather( for location: LocationProtocol, including dataSet: WeatherQuery ) async throws -> T { -#if canImport(CoreLocation) - guard let countryCode = try await geocoder.countryCode(location) else { throw WeatherError.countryCode } - - let _dataSet = dataSet.update(with: countryCode) -#else - let _dataSet = dataSet + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet, countryCode: countryCode, timezone: timezone) + } #endif + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> T { + let _dataSet = countryCode.map { dataSet.update(with: $0) } ?? dataSet + let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, queries: _dataSet, + timezone: timezone, jwt: self.configuration.jwt() ) @@ -180,26 +193,34 @@ final public class WeatherService: Sendable { /// Example usage: /// `let (current, minute) = try await service.weather(for: newYork, including: .current, .minute)` /// +#if canImport(CoreLocation) @inlinable final public func weather( for location: LocationProtocol, including dataSet1: WeatherQuery, _ dataSet2: WeatherQuery ) async throws -> (T1, T2) { -#if canImport(CoreLocation) - guard let countryCode = try await geocoder.countryCode(location) else { throw WeatherError.countryCode } - - let _dataSet1 = dataSet1.update(with: countryCode) - let _dataSet2 = dataSet2.update(with: countryCode) -#else - let _dataSet1 = dataSet1 - let _dataSet2 = dataSet2 + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet1, dataSet2, countryCode: countryCode, timezone: timezone) + } #endif + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> (T1, T2) { + let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 + let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 + let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, queries: _dataSet1, _dataSet2, + timezone: timezone, jwt: self.configuration.jwt() ) @@ -219,6 +240,7 @@ final public class WeatherService: Sendable { /// /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. /// +#if canImport(CoreLocation) @inlinable final public func weather( for location: LocationProtocol, @@ -226,22 +248,29 @@ final public class WeatherService: Sendable { _ dataSet2: WeatherQuery, _ dataSet3: WeatherQuery ) async throws -> (T1, T2, T3) { -#if canImport(CoreLocation) - guard let countryCode = try await geocoder.countryCode(location) else { throw WeatherError.countryCode } - - let _dataSet1 = dataSet1.update(with: countryCode) - let _dataSet2 = dataSet2.update(with: countryCode) - let _dataSet3 = dataSet3.update(with: countryCode) -#else - let _dataSet1 = dataSet1 - let _dataSet2 = dataSet2 - let _dataSet3 = dataSet3 + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, countryCode: countryCode, timezone: timezone) + } #endif + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> (T1, T2, T3) { + let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 + let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 + let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 + let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, queries: _dataSet1, _dataSet2, _dataSet3, + timezone: timezone, jwt: self.configuration.jwt() ) @@ -262,6 +291,7 @@ final public class WeatherService: Sendable { /// /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. /// +#if canImport(CoreLocation) @inlinable final public func weather( for location: LocationProtocol, @@ -270,24 +300,31 @@ final public class WeatherService: Sendable { _ dataSet3: WeatherQuery, _ dataSet4: WeatherQuery ) async throws -> (T1, T2, T3, T4) { -#if canImport(CoreLocation) - guard let countryCode = try await geocoder.countryCode(location) else { throw WeatherError.countryCode } - - let _dataSet1 = dataSet1.update(with: countryCode) - let _dataSet2 = dataSet2.update(with: countryCode) - let _dataSet3 = dataSet3.update(with: countryCode) - let _dataSet4 = dataSet4.update(with: countryCode) -#else - let _dataSet1 = dataSet1 - let _dataSet2 = dataSet2 - let _dataSet3 = dataSet3 - let _dataSet4 = dataSet4 + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, countryCode: countryCode, timezone: timezone) + } #endif - + + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery, + _ dataSet4: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> (T1, T2, T3, T4) { + let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 + let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 + let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 + let _dataSet4 = countryCode.map { dataSet4.update(with: $0) } ?? dataSet4 + let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, + timezone: timezone, jwt: self.configuration.jwt() ) @@ -309,6 +346,7 @@ final public class WeatherService: Sendable { /// /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. /// +#if canImport(CoreLocation) @inlinable final public func weather( for location: LocationProtocol, @@ -318,26 +356,33 @@ final public class WeatherService: Sendable { _ dataSet4: WeatherQuery, _ dataSet5: WeatherQuery ) async throws -> (T1, T2, T3, T4, T5) { -#if canImport(CoreLocation) - guard let countryCode = try await geocoder.countryCode(location) else { throw WeatherError.countryCode } - - let _dataSet1 = dataSet1.update(with: countryCode) - let _dataSet2 = dataSet2.update(with: countryCode) - let _dataSet3 = dataSet3.update(with: countryCode) - let _dataSet4 = dataSet4.update(with: countryCode) - let _dataSet5 = dataSet5.update(with: countryCode) -#else - let _dataSet1 = dataSet1 - let _dataSet2 = dataSet2 - let _dataSet3 = dataSet3 - let _dataSet4 = dataSet4 - let _dataSet5 = dataSet5 + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, dataSet5, countryCode: countryCode, timezone: timezone) + } #endif + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery, + _ dataSet4: WeatherQuery, + _ dataSet5: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> (T1, T2, T3, T4, T5) { + let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 + let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 + let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 + let _dataSet4 = countryCode.map { dataSet4.update(with: $0) } ?? dataSet4 + let _dataSet5 = countryCode.map { dataSet5.update(with: $0) } ?? dataSet5 + let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, _dataSet5, + timezone: timezone, jwt: self.configuration.jwt() ) @@ -360,6 +405,7 @@ final public class WeatherService: Sendable { /// /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. /// +#if canImport(CoreLocation) @inlinable final public func weather( for location: LocationProtocol, @@ -370,28 +416,35 @@ final public class WeatherService: Sendable { _ dataSet5: WeatherQuery, _ dataSet6: WeatherQuery ) async throws -> (T1, T2, T3, T4, T5, T6) { -#if canImport(CoreLocation) - guard let countryCode = try await geocoder.countryCode(location) else { throw WeatherError.countryCode } - - let _dataSet1 = dataSet1.update(with: countryCode) - let _dataSet2 = dataSet2.update(with: countryCode) - let _dataSet3 = dataSet3.update(with: countryCode) - let _dataSet4 = dataSet4.update(with: countryCode) - let _dataSet5 = dataSet5.update(with: countryCode) - let _dataSet6 = dataSet6.update(with: countryCode) -#else - let _dataSet1 = dataSet1 - let _dataSet2 = dataSet2 - let _dataSet3 = dataSet3 - let _dataSet4 = dataSet4 - let _dataSet5 = dataSet5 - let _dataSet6 = dataSet6 + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, dataSet5, dataSet6, countryCode: countryCode, timezone: timezone) + } #endif + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery, + _ dataSet4: WeatherQuery, + _ dataSet5: WeatherQuery, + _ dataSet6: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> (T1, T2, T3, T4, T5, T6) { + let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 + let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 + let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 + let _dataSet4 = countryCode.map { dataSet4.update(with: $0) } ?? dataSet4 + let _dataSet5 = countryCode.map { dataSet5.update(with: $0) } ?? dataSet5 + let _dataSet6 = countryCode.map { dataSet6.update(with: $0) } ?? dataSet6 + let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, _dataSet5, _dataSet6, + timezone: timezone, jwt: self.configuration.jwt() ) @@ -407,17 +460,32 @@ final public class WeatherService: Sendable { } extension WeatherService { +#if canImport(CoreLocation) + @usableFromInline + func resolveCountryCodeAndTimezone(for location: LocationProtocol) async throws -> (countryCode: String, timezone: TimeZone) { + guard let countryCode = try await geocoder.countryCode(location) else { + throw WeatherError.countryCode + } + guard let timezoneIdentifier = try await geocoder.timezone(location), + let timezone = TimeZone(identifier: timezoneIdentifier) else { + throw WeatherError.timezone + } + return (countryCode, timezone) + } +#endif + @usableFromInline - func getWeather(location: LocationProtocol, countryCode: String, language: WeatherService.Configuration.Language? = nil) async throws -> Weather { + func getWeather(location: LocationProtocol, countryCode: String, timezone: TimeZone, language: WeatherService.Configuration.Language? = nil) async throws -> Weather { let proxy = try await networkClient.fetchWeather( location: location, language: language ?? self.configuration.language, queries: WeatherQuery.current, - WeatherQuery?>.minute, - WeatherQuery>.hourly, - WeatherQuery>.daily, - WeatherQuery<[WeatherAlert]?>.alerts(countryCode: countryCode), - WeatherQuery.availability(countryCode: countryCode), + WeatherQuery?>.minute, + WeatherQuery>.hourly, + WeatherQuery>.daily, + WeatherQuery<[WeatherAlert]?>.alerts(countryCode: countryCode), + WeatherQuery.availability(countryCode: countryCode), + timezone: timezone, jwt: self.configuration.jwt() ) diff --git a/Tests/OpenWeatherKitTests/OpenWeatherKitTests.swift b/Tests/OpenWeatherKitTests/OpenWeatherKitTests.swift index c6ff1f7..325b6c2 100644 --- a/Tests/OpenWeatherKitTests/OpenWeatherKitTests.swift +++ b/Tests/OpenWeatherKitTests/OpenWeatherKitTests.swift @@ -27,7 +27,8 @@ final class OpenWeatherKitTests: XCTestCase { longitude: 0 ), countryCode: "US", - language: .germanDE + timezone: TimeZone(secondsFromGMT: 0)!, + language: .englishUS ) } @@ -127,7 +128,8 @@ final class OpenWeatherKitTests: XCTestCase { latitude: 0, longitude: 0 ), - countryCode: "" + countryCode: "", + timezone: TimeZone(secondsFromGMT: 0)! ) } @@ -145,7 +147,9 @@ final class OpenWeatherKitTests: XCTestCase { for: Location( latitude: 0, longitude: 0), - including: .daily) + including: .daily, + timezone: TimeZone(secondsFromGMT: 0)! + ) } func testDataSet2() async throws { @@ -162,7 +166,9 @@ final class OpenWeatherKitTests: XCTestCase { for: Location( latitude: 0, longitude: 0), - including: .daily, .hourly) + including: .daily, .hourly, + timezone: TimeZone(secondsFromGMT: 0)! + ) } func testDataSet3() async throws { @@ -179,7 +185,9 @@ final class OpenWeatherKitTests: XCTestCase { for: Location( latitude: 0, longitude: 0), - including: .daily, .hourly, .alerts(countryCode: "")) + including: .daily, .hourly, .alerts(countryCode: ""), + timezone: TimeZone(secondsFromGMT: 0)! + ) } func testDataSet3Optional() async throws { @@ -197,7 +205,9 @@ final class OpenWeatherKitTests: XCTestCase { for: Location( latitude: 0, longitude: 0), - including: .daily, .hourly, .alerts(countryCode: "")) + including: .daily, .hourly, .alerts(countryCode: ""), + timezone: TimeZone(secondsFromGMT: 0)! + ) } catch { switch error { case let weatherError as WeatherError where weatherError == .missingData(APIWeather.CodingKeys.weatherAlerts.rawValue): diff --git a/Tests/OpenWeatherKitTests/Utils/Geocoder+Mock.swift b/Tests/OpenWeatherKitTests/Utils/Geocoder+Mock.swift index a825839..b3a8225 100644 --- a/Tests/OpenWeatherKitTests/Utils/Geocoder+Mock.swift +++ b/Tests/OpenWeatherKitTests/Utils/Geocoder+Mock.swift @@ -10,6 +10,9 @@ import Foundation #if canImport(CoreLocation) extension Geocoder { - static let mock = Self(countryCode: { _ in "" }) + static let mock = Self( + countryCode: { _ in "" }, + timezone: { _ in TimeZone(secondsFromGMT: 0)!.identifier } + ) } #endif diff --git a/Tests/OpenWeatherKitTests/Utils/MockClient.swift b/Tests/OpenWeatherKitTests/Utils/MockClient.swift index 6d2a91a..ca16d22 100644 --- a/Tests/OpenWeatherKitTests/Utils/MockClient.swift +++ b/Tests/OpenWeatherKitTests/Utils/MockClient.swift @@ -31,10 +31,10 @@ actor MockClient: Client { #if os(Linux) func execute(_ request: HTTPClientRequest, timeout: TimeAmount) async throws -> HTTPClientResponse { let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601 + encoder.dateEncodingStrategy = .secondsSince1970 let buffer: ByteBuffer = { - if request.url.contains("/api/v1/availability/") { + if request.url.contains("/availability/") { return try! encoder.encodeAsByteBuffer( MockData.availability, allocator: .init() @@ -54,10 +54,10 @@ actor MockClient: Client { #else func data(_ request: URLRequest) async throws -> (Data, URLResponse) { let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601 + encoder.dateEncodingStrategy = .secondsSince1970 let data: Data = { - if request.url!.absoluteString.contains("/api/v1/availability/") { + if request.url!.absoluteString.contains("/availability/") { return try! encoder.encode(MockData.availability) } else { return try! encoder.encode(Self.apiWeather(with: include)) diff --git a/Tests/OpenWeatherKitTests/Utils/MockData.swift b/Tests/OpenWeatherKitTests/Utils/MockData.swift index 17ab3ac..43bbe89 100644 --- a/Tests/OpenWeatherKitTests/Utils/MockData.swift +++ b/Tests/OpenWeatherKitTests/Utils/MockData.swift @@ -37,7 +37,7 @@ struct MockData { fileprivate extension MockData { static var jsonDecoder: JSONDecoder { let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 + decoder.dateDecodingStrategy = .secondsSince1970 return decoder } @@ -50,23 +50,23 @@ fileprivate extension MockData { """ static let currentWeatherJSON = """ - {"name":"CurrentWeather","metadata":{"attributionURL":"https://weatherkit.apple.com/legal-attribution.html","expireTime":"2022-11-07T15:19:51Z","latitude":37.523,"longitude":-77.573,"readTime":"2022-11-07T15:14:51Z","reportedTime":"2022-11-07T14:00:00Z","units":"m","version":1},"asOf":"2022-11-07T15:14:51Z","cloudCover":0.07,"conditionCode":"Clear","daylight":true,"humidity":0.74,"precipitationIntensity":0.0,"pressure":1024.49,"pressureTrend":"rising","temperature":23.32,"temperatureApparent":24.40,"temperatureDewPoint":18.35,"uvIndex":3,"visibility":29408.91,"windDirection":0,"windGust":19.62,"windSpeed":8.59} + {"metadata":{"latitude":40.709999,"longitude":-74.010002,"attributionUrl":"https://developer.apple.com/weatherkit/data-source-attribution/","readTime":1760992745,"expireTime":1760993045,"temporarilyUnavailable":false,"sourceType":"MODELED","reportedTime":1760992745},"daylight":true,"humidity":59,"pressure":1009.795837,"temperature":16.504652,"visibility":34580.664062,"snowfallAmount24h":0.0,"asOf":1760992745,"snowfallAmount6h":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":8.464172,"windGust":48.896671,"temperatureApparent":11.724845,"precipitationAmountNext6hByType":[],"snowfallAmountNext24h":0.0,"precipitationAmountPrevious6hByType":[],"pressureTrend":"RISING","windDirection":276,"precipitationAmount1h":0.0,"windSpeed":24.742052,"precipitationAmountNext6h":0.0,"snowfallAmount1h":0.0,"precipitationAmount6h":0.0,"cloudCoverLowAltPct":41,"perceivedPrecipitationIntensity":0.0,"precipitationAmountPrevious1hByType":[],"cloudCover":67,"precipitationAmountNext1hByType":[],"snowfallAmountNext1h":0.0,"precipitationAmountPrevious24hByType":[{"expected":1.426,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"cloudCoverMidAltPct":47,"conditionCode":"WINDY","cloudCoverHighAltPct":0,"snowfallAmountNext6h":0.0,"precipitationAmount24h":1.426,"precipitationAmountNext1h":0.0,"precipitationAmountNext24h":0.0,"uvIndex":0,"precipitationAmountNext24hByType":[]} """ static let dailyWeatherJSON = """ - {"name":"DailyForecast","metadata":{"attributionURL":"https://weatherkit.apple.com/legal-attribution.html","expireTime":"2022-11-07T16:14:51Z","latitude":37.523,"longitude":-77.573,"readTime":"2022-11-07T15:14:51Z","reportedTime":"2022-11-07T14:00:00Z","units":"m","version":1},"days":[{"forecastStart":"2022-11-07T05:00:00Z","forecastEnd":"2022-11-08T05:00:00Z","conditionCode":"Clear","maxUvIndex":4,"moonPhase":"full","moonrise":"2022-11-07T21:44:54Z","moonset":"2022-11-07T10:43:55Z","precipitationAmount":0.0,"precipitationChance":0.06,"precipitationType":"clear","snowfallAmount":0.00,"solarMidnight":"2022-11-08T04:54:08Z","solarNoon":"2022-11-07T16:53:49Z","sunrise":"2022-11-07T11:41:38Z","sunriseCivil":"2022-11-07T11:14:13Z","sunriseNautical":"2022-11-07T10:43:03Z","sunriseAstronomical":"2022-11-07T10:12:20Z","sunset":"2022-11-07T22:06:04Z","sunsetCivil":"2022-11-07T22:33:32Z","sunsetNautical":"2022-11-07T23:04:36Z","sunsetAstronomical":"2022-11-07T23:35:21Z","temperatureMax":26.55,"temperatureMin":14.25,"daytimeForecast":{"forecastStart":"2022-11-07T12:00:00Z","forecastEnd":"2022-11-08T00:00:00Z","cloudCover":0.11,"conditionCode":"Clear","humidity":0.60,"precipitationAmount":0.0,"precipitationChance":0.02,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":11,"windSpeed":8.58},"overnightForecast":{"forecastStart":"2022-11-08T00:00:00Z","forecastEnd":"2022-11-08T12:00:00Z","cloudCover":0.04,"conditionCode":"Clear","humidity":0.55,"precipitationAmount":0.0,"precipitationChance":0.00,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":11,"windSpeed":15.23},"restOfDayForecast":{"forecastStart":"2022-11-07T15:14:51Z","forecastEnd":"2022-11-08T05:00:00Z","cloudCover":0.09,"conditionCode":"Clear","humidity":0.53,"precipitationAmount":0.0,"precipitationChance":0.00,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":16,"windSpeed":10.72}},{"forecastStart":"2022-11-08T05:00:00Z","forecastEnd":"2022-11-09T05:00:00Z","conditionCode":"Clear","maxUvIndex":4,"moonPhase":"full","moonrise":"2022-11-08T22:14:26Z","moonset":"2022-11-08T11:49:02Z","precipitationAmount":0.0,"precipitationChance":0.00,"precipitationType":"clear","snowfallAmount":0.00,"solarMidnight":"2022-11-09T04:54:12Z","solarNoon":"2022-11-08T16:53:53Z","sunrise":"2022-11-08T11:42:42Z","sunriseCivil":"2022-11-08T11:15:13Z","sunriseNautical":"2022-11-08T10:44:00Z","sunriseAstronomical":"2022-11-08T10:13:14Z","sunset":"2022-11-08T22:05:09Z","sunsetCivil":"2022-11-08T22:32:41Z","sunsetNautical":"2022-11-08T23:03:48Z","sunsetAstronomical":"2022-11-08T23:34:35Z","temperatureMax":15.88,"temperatureMin":7.46,"daytimeForecast":{"forecastStart":"2022-11-08T12:00:00Z","forecastEnd":"2022-11-09T00:00:00Z","cloudCover":0.01,"conditionCode":"Clear","humidity":0.50,"precipitationAmount":0.0,"precipitationChance":0.00,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":23,"windSpeed":16.62},"overnightForecast":{"forecastStart":"2022-11-09T00:00:00Z","forecastEnd":"2022-11-09T12:00:00Z","cloudCover":0.02,"conditionCode":"Clear","humidity":0.71,"precipitationAmount":0.0,"precipitationChance":0.00,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":18,"windSpeed":12.02}},{"forecastStart":"2022-11-09T05:00:00Z","forecastEnd":"2022-11-10T05:00:00Z","conditionCode":"MostlyClear","maxUvIndex":4,"moonPhase":"full","moonrise":"2022-11-09T22:48:13Z","moonset":"2022-11-09T12:53:49Z","precipitationAmount":0.0,"precipitationChance":0.00,"precipitationType":"clear","snowfallAmount":0.00,"solarMidnight":"2022-11-10T04:54:17Z","solarNoon":"2022-11-09T16:53:58Z","sunrise":"2022-11-09T11:43:45Z","sunriseCivil":"2022-11-09T11:16:13Z","sunriseNautical":"2022-11-09T10:44:56Z","sunriseAstronomical":"2022-11-09T10:14:09Z","sunset":"2022-11-09T22:04:16Z","sunsetCivil":"2022-11-09T22:31:51Z","sunsetNautical":"2022-11-09T23:03:01Z","sunsetAstronomical":"2022-11-09T23:33:50Z","temperatureMax":16.62,"temperatureMin":4.97,"daytimeForecast":{"forecastStart":"2022-11-09T12:00:00Z","forecastEnd":"2022-11-10T00:00:00Z","cloudCover":0.20,"conditionCode":"MostlyClear","humidity":0.61,"precipitationAmount":0.0,"precipitationChance":0.00,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":43,"windSpeed":11.83},"overnightForecast":{"forecastStart":"2022-11-10T00:00:00Z","forecastEnd":"2022-11-10T12:00:00Z","cloudCover":0.46,"conditionCode":"PartlyCloudy","humidity":0.85,"precipitationAmount":0.0,"precipitationChance":0.02,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":17,"windSpeed":8.23}},{"forecastStart":"2022-11-10T05:00:00Z","forecastEnd":"2022-11-11T05:00:00Z","conditionCode":"MostlyCloudy","maxUvIndex":3,"moonPhase":"full","moonrise":"2022-11-10T23:27:23Z","moonset":"2022-11-10T13:57:08Z","precipitationAmount":0.0,"precipitationChance":0.18,"precipitationType":"clear","snowfallAmount":0.00,"solarMidnight":"2022-11-11T04:54:23Z","solarNoon":"2022-11-10T16:54:04Z","sunrise":"2022-11-10T11:44:48Z","sunriseCivil":"2022-11-10T11:17:13Z","sunriseNautical":"2022-11-10T10:45:53Z","sunriseAstronomical":"2022-11-10T10:15:04Z","sunset":"2022-11-10T22:03:24Z","sunsetCivil":"2022-11-10T22:31:03Z","sunsetNautical":"2022-11-10T23:02:16Z","sunsetAstronomical":"2022-11-10T23:33:08Z","temperatureMax":20.26,"temperatureMin":8.11,"daytimeForecast":{"forecastStart":"2022-11-10T12:00:00Z","forecastEnd":"2022-11-11T00:00:00Z","cloudCover":0.76,"conditionCode":"MostlyCloudy","humidity":0.71,"precipitationAmount":0.0,"precipitationChance":0.10,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":61,"windSpeed":8.48},"overnightForecast":{"forecastStart":"2022-11-11T00:00:00Z","forecastEnd":"2022-11-11T12:00:00Z","cloudCover":0.93,"conditionCode":"Drizzle","humidity":0.89,"precipitationAmount":2.2,"precipitationChance":0.37,"precipitationType":"rain","snowfallAmount":0.00,"windDirection":68,"windSpeed":7.39}},{"forecastStart":"2022-11-11T05:00:00Z","forecastEnd":"2022-11-12T05:00:00Z","conditionCode":"Thunderstorms","maxUvIndex":3,"moonPhase":"waningGibbous","moonrise":"2022-11-12T00:12:36Z","moonset":"2022-11-11T14:57:07Z","precipitationAmount":24.2,"precipitationChance":0.81,"precipitationType":"rain","snowfallAmount":0.00,"solarMidnight":"2022-11-12T04:54:30Z","solarNoon":"2022-11-11T16:54:10Z","sunrise":"2022-11-11T11:45:51Z","sunriseCivil":"2022-11-11T11:18:13Z","sunriseNautical":"2022-11-11T10:46:50Z","sunriseAstronomical":"2022-11-11T10:15:59Z","sunset":"2022-11-11T22:02:34Z","sunsetCivil":"2022-11-11T22:30:17Z","sunsetNautical":"2022-11-11T23:01:33Z","sunsetAstronomical":"2022-11-11T23:32:27Z","temperatureMax":21.49,"temperatureMin":13.87,"daytimeForecast":{"forecastStart":"2022-11-11T12:00:00Z","forecastEnd":"2022-11-12T00:00:00Z","cloudCover":0.98,"conditionCode":"Thunderstorms","humidity":0.88,"precipitationAmount":17.9,"precipitationChance":0.76,"precipitationType":"rain","snowfallAmount":0.00,"windDirection":125,"windSpeed":10.37},"overnightForecast":{"forecastStart":"2022-11-12T00:00:00Z","forecastEnd":"2022-11-12T12:00:00Z","cloudCover":0.96,"conditionCode":"Thunderstorms","humidity":0.91,"precipitationAmount":21.9,"precipitationChance":0.72,"precipitationType":"rain","snowfallAmount":0.00,"windDirection":180,"windSpeed":13.60}},{"forecastStart":"2022-11-12T05:00:00Z","forecastEnd":"2022-11-13T05:00:00Z","conditionCode":"Thunderstorms","maxUvIndex":4,"moonPhase":"waningGibbous","moonrise":"2022-11-13T01:03:50Z","moonset":"2022-11-12T15:51:45Z","precipitationAmount":23.8,"precipitationChance":0.73,"precipitationType":"rain","snowfallAmount":0.00,"solarMidnight":"2022-11-13T04:54:38Z","solarNoon":"2022-11-12T16:54:18Z","sunrise":"2022-11-12T11:46:55Z","sunriseCivil":"2022-11-12T11:19:13Z","sunriseNautical":"2022-11-12T10:47:46Z","sunriseAstronomical":"2022-11-12T10:16:54Z","sunset":"2022-11-12T22:01:45Z","sunsetCivil":"2022-11-12T22:29:32Z","sunsetNautical":"2022-11-12T23:00:51Z","sunsetAstronomical":"2022-11-12T23:31:47Z","temperatureMax":21.43,"temperatureMin":7.74,"daytimeForecast":{"forecastStart":"2022-11-12T12:00:00Z","forecastEnd":"2022-11-13T00:00:00Z","cloudCover":0.39,"conditionCode":"Rain","humidity":0.65,"precipitationAmount":6.1,"precipitationChance":0.48,"precipitationType":"rain","snowfallAmount":0.00,"windDirection":270,"windSpeed":12.95},"overnightForecast":{"forecastStart":"2022-11-13T00:00:00Z","forecastEnd":"2022-11-13T12:00:00Z","cloudCover":0.38,"conditionCode":"PartlyCloudy","humidity":0.64,"precipitationAmount":0.0,"precipitationChance":0.08,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":323,"windSpeed":9.50}},{"forecastStart":"2022-11-13T05:00:00Z","forecastEnd":"2022-11-14T05:00:00Z","conditionCode":"PartlyCloudy","maxUvIndex":3,"moonPhase":"waningGibbous","moonrise":"2022-11-14T01:59:56Z","moonset":"2022-11-13T16:39:40Z","precipitationAmount":0.0,"precipitationChance":0.21,"precipitationType":"clear","snowfallAmount":0.00,"solarMidnight":"2022-11-14T04:54:46Z","solarNoon":"2022-11-13T16:54:26Z","sunrise":"2022-11-13T11:47:58Z","sunriseCivil":"2022-11-13T11:20:13Z","sunriseNautical":"2022-11-13T10:48:43Z","sunriseAstronomical":"2022-11-13T10:17:49Z","sunset":"2022-11-13T22:00:58Z","sunsetCivil":"2022-11-13T22:28:49Z","sunsetNautical":"2022-11-13T23:00:11Z","sunsetAstronomical":"2022-11-13T23:31:10Z","temperatureMax":10.26,"temperatureMin":2.30,"daytimeForecast":{"forecastStart":"2022-11-13T12:00:00Z","forecastEnd":"2022-11-14T00:00:00Z","cloudCover":0.53,"conditionCode":"PartlyCloudy","humidity":0.58,"precipitationAmount":0.0,"precipitationChance":0.12,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":277,"windSpeed":6.45},"overnightForecast":{"forecastStart":"2022-11-14T00:00:00Z","forecastEnd":"2022-11-14T12:00:00Z","cloudCover":0.09,"conditionCode":"Clear","humidity":0.79,"precipitationAmount":0.0,"precipitationChance":0.09,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":306,"windSpeed":6.62}},{"forecastStart":"2022-11-14T05:00:00Z","forecastEnd":"2022-11-15T05:00:00Z","conditionCode":"Clear","maxUvIndex":3,"moonPhase":"waningGibbous","moonrise":"2022-11-15T02:59:04Z","moonset":"2022-11-14T17:20:37Z","precipitationAmount":0.0,"precipitationChance":0.12,"precipitationType":"clear","snowfallAmount":0.00,"solarMidnight":"2022-11-15T04:54:56Z","solarNoon":"2022-11-14T16:54:36Z","sunrise":"2022-11-14T11:49:01Z","sunriseCivil":"2022-11-14T11:21:13Z","sunriseNautical":"2022-11-14T10:49:39Z","sunriseAstronomical":"2022-11-14T10:18:43Z","sunset":"2022-11-14T22:00:13Z","sunsetCivil":"2022-11-14T22:28:08Z","sunsetNautical":"2022-11-14T22:59:34Z","sunsetAstronomical":"2022-11-14T23:30:34Z","temperatureMax":9.89,"temperatureMin":-0.77,"daytimeForecast":{"forecastStart":"2022-11-14T12:00:00Z","forecastEnd":"2022-11-15T00:00:00Z","cloudCover":0.10,"conditionCode":"Clear","humidity":0.59,"precipitationAmount":0.0,"precipitationChance":0.08,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":258,"windSpeed":5.88},"overnightForecast":{"forecastStart":"2022-11-15T00:00:00Z","forecastEnd":"2022-11-15T12:00:00Z","cloudCover":0.28,"conditionCode":"MostlyClear","humidity":0.80,"precipitationAmount":0.0,"precipitationChance":0.05,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":12,"windSpeed":5.58}},{"forecastStart":"2022-11-15T05:00:00Z","forecastEnd":"2022-11-16T05:00:00Z","conditionCode":"MostlyClear","maxUvIndex":3,"moonPhase":"thirdQuarter","moonrise":"2022-11-16T03:59:32Z","moonset":"2022-11-15T17:55:23Z","precipitationAmount":0.0,"precipitationChance":0.14,"precipitationType":"clear","snowfallAmount":0.00,"solarMidnight":"2022-11-16T04:55:06Z","solarNoon":"2022-11-15T16:54:46Z","sunrise":"2022-11-15T11:50:04Z","sunriseCivil":"2022-11-15T11:22:13Z","sunriseNautical":"2022-11-15T10:50:36Z","sunriseAstronomical":"2022-11-15T10:19:38Z","sunset":"2022-11-15T21:59:31Z","sunsetCivil":"2022-11-15T22:27:28Z","sunsetNautical":"2022-11-15T22:58:58Z","sunsetAstronomical":"2022-11-15T23:30:00Z","temperatureMax":10.82,"temperatureMin":-1.34,"daytimeForecast":{"forecastStart":"2022-11-15T12:00:00Z","forecastEnd":"2022-11-16T00:00:00Z","cloudCover":0.25,"conditionCode":"MostlyClear","humidity":0.63,"precipitationAmount":0.0,"precipitationChance":0.08,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":79,"windSpeed":4.45},"overnightForecast":{"forecastStart":"2022-11-16T00:00:00Z","forecastEnd":"2022-11-16T12:00:00Z","cloudCover":0.40,"conditionCode":"PartlyCloudy","humidity":0.89,"precipitationAmount":0.0,"precipitationChance":0.09,"precipitationType":"clear","snowfallAmount":0.00,"windDirection":123,"windSpeed":5.79}},{"forecastStart":"2022-11-16T05:00:00Z","forecastEnd":"2022-11-17T05:00:00Z","conditionCode":"Drizzle","maxUvIndex":3,"moonPhase":"thirdQuarter","moonset":"2022-11-16T18:25:26Z","precipitationAmount":2.6,"precipitationChance":0.31,"precipitationType":"rain","snowfallAmount":0.00,"solarMidnight":"2022-11-17T04:55:17Z","solarNoon":"2022-11-16T16:54:57Z","sunrise":"2022-11-16T11:51:07Z","sunriseCivil":"2022-11-16T11:23:13Z","sunriseNautical":"2022-11-16T10:51:32Z","sunriseAstronomical":"2022-11-16T10:20:32Z","sunset":"2022-11-16T21:58:50Z","sunsetCivil":"2022-11-16T22:26:51Z","sunsetNautical":"2022-11-16T22:58:24Z","sunsetAstronomical":"2022-11-16T23:29:28Z","temperatureMax":12.01,"temperatureMin":1.09,"daytimeForecast":{"forecastStart":"2022-11-16T12:00:00Z","forecastEnd":"2022-11-17T00:00:00Z","cloudCover":0.72,"conditionCode":"MostlyCloudy","humidity":0.68,"precipitationAmount":1.8,"precipitationChance":0.21,"precipitationType":"rain","snowfallAmount":0.00,"windDirection":265,"windSpeed":7.63},"overnightForecast":{"forecastStart":"2022-11-17T00:00:00Z","forecastEnd":"2022-11-17T12:00:00Z","cloudCover":1.00,"conditionCode":"Cloudy","humidity":0.82,"precipitationAmount":3.0,"precipitationChance":0.27,"precipitationType":"rain","snowfallAmount":0.00,"windDirection":208,"windSpeed":4.70}}]} + {"metadata":{"latitude":40.709999,"longitude":-74.010002,"attributionUrl":"https://developer.apple.com/weatherkit/data-source-attribution/","readTime":1760992827,"expireTime":1760996295,"temporarilyUnavailable":false,"sourceType":"MODELED","reportedTime":1760961934},"days":[{"moonrise":1760955677,"moonset":1760995956,"sunrise":1760958786,"sunset":1760998079,"forecastEnd":1761019200,"sunriseCivil":1760957122,"snowfallAmount":0.0,"temperatureMax":19.604397,"solarMidnight":1760935261,"windSpeedAvg":20.323332,"temperatureMaxTime":1760940462,"windGustSpeedMax":56.481377,"precipitationAmount":1.426,"sunriseAstronomical":1760953309,"forecastStart":1760932800,"moonPhase":"WANING_CRESCENT","humidityMax":88,"precipitationAmountByType":[{"expected":1.426,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"sunsetAstronomical":1761003557,"visibilityMax":34655.355469,"precipitationChance":80,"temperatureMin":12.16,"maxUvIndex":3,"temperatureMinTime":1761019200,"sunsetNautical":1761001653,"daytimeForecast":{"humidity":68,"daylight":true,"forecastEnd":1761001200,"snowfallAmount":0.0,"temperatureMax":16.590218,"uvIndexMin":0,"temperatureApparentMin":8.725781,"windGustSpeedMax":56.481377,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":13.272897,"precipitationAmount":0.0,"forecastStart":1760958000,"windDirection":259,"windSpeed":24.186375,"humidityMax":87,"precipitationAmountByType":[],"cloudCoverLowAltPct":60,"visibilityMax":34655.355469,"cloudCover":67,"precipitationChance":0,"temperatureMin":14.034677,"cloudCoverMidAltPct":31,"precipitationIntensityMax":0.0,"conditionCode":"WINDY","cloudCoverHighAltPct":0,"humidityMin":58,"visibilityMin":17583.0,"precipitationType":"CLEAR","windSpeedMax":28.665266},"conditionCode":"RAIN","humidityMin":58,"solarNoon":1760978432,"visibilityMin":12018.388672,"overnightForecast":{"humidity":68,"daylight":false,"forecastEnd":1761044400,"snowfallAmount":0.0,"temperatureMax":15.22,"uvIndexMin":0,"temperatureApparentMin":5.715589,"windGustSpeedMax":38.051998,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":10.461295,"precipitationAmount":0.0,"forecastStart":1761001200,"windDirection":261,"windSpeed":14.514926,"humidityMax":76,"precipitationAmountByType":[],"cloudCoverLowAltPct":3,"visibilityMax":34123.0,"cloudCover":4,"precipitationChance":0,"temperatureMin":9.48,"cloudCoverMidAltPct":0,"precipitationIntensityMax":0.0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"humidityMin":60,"visibilityMin":31188.0,"precipitationType":"CLEAR","windSpeedMax":19.027884},"precipitationType":"RAIN","sunriseNautical":1760955207,"sunsetCivil":1760999748,"windSpeedMax":28.665266,"restOfDayForecast":{"humidity":62,"daylight":false,"forecastEnd":1761019200,"snowfallAmount":0.0,"temperatureMax":16.498795,"uvIndexMin":0,"temperatureApparentMin":7.687512,"windGustSpeedMax":48.869514,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":11.805491,"precipitationAmount":0.0,"forecastStart":1760992827,"windDirection":269,"windSpeed":18.297956,"humidityMax":67,"precipitationAmountByType":[],"cloudCoverLowAltPct":13,"visibilityMax":34655.355469,"cloudCover":19,"precipitationChance":0,"temperatureMin":12.16,"cloudCoverMidAltPct":7,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"humidityMin":58,"visibilityMin":32719.0,"precipitationType":"CLEAR","windSpeedMax":24.704273}},{"moonrise":1761045789,"moonset":1761083664,"sunrise":1761045253,"sunset":1761084393,"forecastEnd":1761105600,"sunriseCivil":1761043586,"snowfallAmount":0.0,"temperatureMax":19.11182,"solarMidnight":1761021651,"windSpeedAvg":13.211013,"temperatureMaxTime":1761076535,"windGustSpeedMax":33.994553,"precipitationAmount":1.693,"sunriseAstronomical":1761039771,"forecastStart":1761019200,"moonPhase":"NEW","humidityMax":78,"precipitationAmountByType":[{"expected":1.693,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"sunsetAstronomical":1761089876,"visibilityMax":33198.886719,"precipitationChance":13,"temperatureMin":9.479772,"maxUvIndex":4,"temperatureMinTime":1761044666,"sunsetNautical":1761087972,"daytimeForecast":{"humidity":59,"daylight":true,"forecastEnd":1761087600,"snowfallAmount":0.0,"temperatureMax":19.11182,"uvIndexMin":0,"temperatureApparentMin":5.913912,"windGustSpeedMax":33.994553,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":4,"temperatureApparentMax":18.34053,"precipitationAmount":0.0,"forecastStart":1761044400,"windDirection":203,"windSpeed":13.429988,"humidityMax":76,"precipitationAmountByType":[],"cloudCoverLowAltPct":0,"visibilityMax":33198.886719,"cloudCover":5,"precipitationChance":0,"temperatureMin":9.479772,"cloudCoverMidAltPct":3,"precipitationIntensityMax":0.0,"conditionCode":"CLEAR","cloudCoverHighAltPct":6,"humidityMin":48,"visibilityMin":29599.0,"precipitationType":"CLEAR","windSpeedMax":16.954836},"conditionCode":"CLEAR","humidityMin":48,"solarNoon":1761064823,"visibilityMin":23130.0,"overnightForecast":{"humidity":80,"daylight":false,"forecastEnd":1761130800,"snowfallAmount":0.0,"temperatureMax":17.129999,"uvIndexMin":0,"temperatureApparentMin":10.810381,"windGustSpeedMax":33.079964,"perceivedPrecipitationIntensityMax":1.29,"uvIndexMax":0,"temperatureApparentMax":14.194607,"precipitationAmount":4.369,"forecastStart":1761087600,"windDirection":199,"windSpeed":10.360663,"humidityMax":91,"precipitationAmountByType":[{"expected":4.368999,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"cloudCoverLowAltPct":26,"visibilityMax":31297.451172,"cloudCover":69,"precipitationChance":38,"temperatureMin":12.25,"cloudCoverMidAltPct":58,"precipitationIntensityMax":1.694435,"conditionCode":"RAIN","cloudCoverHighAltPct":13,"humidityMin":63,"visibilityMin":14850.519531,"precipitationType":"RAIN","windSpeedMax":14.436001},"precipitationType":"RAIN","sunriseNautical":1761041670,"sunsetCivil":1761086065,"windSpeedMax":16.954836},{"moonrise":1761135949,"moonset":1761171569,"sunrise":1761131720,"sunset":1761170708,"forecastEnd":1761192000,"sunriseCivil":1761130050,"snowfallAmount":0.0,"temperatureMax":16.530001,"solarMidnight":1761108041,"windSpeedAvg":14.159187,"temperatureMaxTime":1761156000,"windGustSpeedMax":41.528694,"precipitationAmount":2.676,"sunriseAstronomical":1761126232,"forecastStart":1761105600,"moonPhase":"WAXING_CRESCENT","humidityMax":91,"precipitationAmountByType":[{"expected":2.676,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"sunsetAstronomical":1761176196,"visibilityMax":32597.847656,"precipitationChance":47,"temperatureMin":10.43,"maxUvIndex":3,"temperatureMinTime":1761192000,"sunsetNautical":1761174291,"daytimeForecast":{"humidity":62,"daylight":true,"forecastEnd":1761174000,"snowfallAmount":0.0,"temperatureMax":16.530001,"uvIndexMin":0,"temperatureApparentMin":8.525494,"windGustSpeedMax":41.528694,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":13.53885,"precipitationAmount":0.0,"forecastStart":1761130800,"windDirection":252,"windSpeed":17.481287,"humidityMax":91,"precipitationAmountByType":[],"cloudCoverLowAltPct":11,"visibilityMax":32597.847656,"cloudCover":27,"precipitationChance":0,"temperatureMin":12.223086,"cloudCoverMidAltPct":13,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"humidityMin":46,"visibilityMin":24048.089844,"precipitationType":"CLEAR","windSpeedMax":21.900928},"conditionCode":"DRIZZLE","humidityMin":46,"solarNoon":1761151214,"visibilityMin":14850.519531,"overnightForecast":{"humidity":69,"daylight":false,"forecastEnd":1761217200,"snowfallAmount":0.0,"temperatureMax":13.46,"uvIndexMin":0,"temperatureApparentMin":4.223551,"windGustSpeedMax":34.549198,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":8.525494,"precipitationAmount":0.0,"forecastStart":1761174000,"windDirection":246,"windSpeed":13.903475,"humidityMax":79,"precipitationAmountByType":[],"cloudCoverLowAltPct":1,"visibilityMax":31350.363281,"cloudCover":7,"precipitationChance":0,"temperatureMin":8.479733,"cloudCoverMidAltPct":4,"precipitationIntensityMax":0.0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"humidityMin":55,"visibilityMin":29032.0,"precipitationType":"CLEAR","windSpeedMax":16.559999},"precipitationType":"RAIN","sunriseNautical":1761128132,"sunsetCivil":1761172383,"windSpeedMax":21.900928},{"moonrise":1761226141,"moonset":1761259754,"sunrise":1761218188,"sunset":1761257023,"forecastEnd":1761278400,"sunriseCivil":1761216514,"snowfallAmount":0.0,"temperatureMax":14.238524,"solarMidnight":1761194433,"windSpeedAvg":14.478049,"temperatureMaxTime":1761244829,"windGustSpeedMax":35.729374,"precipitationAmount":0.0,"sunriseAstronomical":1761212694,"forecastStart":1761192000,"moonPhase":"WAXING_CRESCENT","humidityMax":79,"precipitationAmountByType":[],"sunsetAstronomical":1761262518,"visibilityMax":32900.125,"precipitationChance":0,"temperatureMin":8.479733,"maxUvIndex":3,"temperatureMinTime":1761217103,"sunsetNautical":1761260612,"daytimeForecast":{"humidity":62,"daylight":true,"forecastEnd":1761260400,"snowfallAmount":0.0,"temperatureMax":14.238524,"uvIndexMin":0,"temperatureApparentMin":4.345078,"windGustSpeedMax":35.729374,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":10.832659,"precipitationAmount":0.0,"forecastStart":1761217200,"windDirection":252,"windSpeed":16.204586,"humidityMax":79,"precipitationAmountByType":[],"cloudCoverLowAltPct":28,"visibilityMax":31982.833984,"cloudCover":42,"precipitationChance":0,"temperatureMin":8.48,"cloudCoverMidAltPct":31,"precipitationIntensityMax":0.0,"conditionCode":"PARTLY_CLOUDY","cloudCoverHighAltPct":0,"humidityMin":54,"visibilityMin":28920.330078,"precipitationType":"CLEAR","windSpeedMax":18.524895},"conditionCode":"PARTLY_CLOUDY","humidityMin":54,"solarNoon":1761237606,"visibilityMin":28920.330078,"overnightForecast":{"humidity":77,"daylight":false,"forecastEnd":1761303600,"snowfallAmount":0.0,"temperatureMax":12.3,"uvIndexMin":0,"temperatureApparentMin":3.822799,"windGustSpeedMax":29.8836,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":8.884628,"precipitationAmount":0.0,"forecastStart":1761260400,"windDirection":268,"windSpeed":10.912325,"humidityMax":85,"precipitationAmountByType":[],"cloudCoverLowAltPct":4,"visibilityMax":32900.125,"cloudCover":7,"precipitationChance":0,"temperatureMin":6.71,"cloudCoverMidAltPct":4,"precipitationIntensityMax":0.0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"humidityMin":62,"visibilityMin":27987.894531,"precipitationType":"CLEAR","windSpeedMax":12.4452},"precipitationType":"RAIN","sunriseNautical":1761214595,"sunsetCivil":1761258702,"windSpeedMax":18.524895},{"moonrise":1761316292,"moonset":1761348320,"sunrise":1761304656,"sunset":1761343340,"forecastEnd":1761364800,"sunriseCivil":1761302978,"snowfallAmount":0.0,"temperatureMax":14.517237,"solarMidnight":1761280824,"windSpeedAvg":10.642105,"temperatureMaxTime":1761331309,"windGustSpeedMax":30.524885,"precipitationAmount":0.0,"sunriseAstronomical":1761299156,"forecastStart":1761278400,"moonPhase":"WAXING_CRESCENT","humidityMax":85,"precipitationAmountByType":[],"sunsetAstronomical":1761348842,"visibilityMax":34697.878906,"precipitationChance":0,"temperatureMin":6.663746,"maxUvIndex":3,"temperatureMinTime":1761305412,"sunsetNautical":1761346934,"daytimeForecast":{"humidity":61,"daylight":true,"forecastEnd":1761346800,"snowfallAmount":0.0,"temperatureMax":14.517237,"uvIndexMin":0,"temperatureApparentMin":3.941025,"windGustSpeedMax":30.524885,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":12.482799,"precipitationAmount":0.0,"forecastStart":1761303600,"windDirection":284,"windSpeed":11.7716,"humidityMax":85,"precipitationAmountByType":[],"cloudCoverLowAltPct":21,"visibilityMax":34530.664062,"cloudCover":33,"precipitationChance":0,"temperatureMin":6.663746,"cloudCoverMidAltPct":29,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"humidityMin":48,"visibilityMin":27989.0,"precipitationType":"CLEAR","windSpeedMax":14.552842},"conditionCode":"MOSTLY_CLEAR","humidityMin":48,"solarNoon":1761323998,"visibilityMin":27987.894531,"overnightForecast":{"humidity":74,"daylight":false,"forecastEnd":1761390000,"snowfallAmount":0.0,"temperatureMax":12.1,"uvIndexMin":0,"temperatureApparentMin":4.741363,"windGustSpeedMax":24.6996,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":9.294739,"precipitationAmount":0.0,"forecastStart":1761346800,"windDirection":293,"windSpeed":7.757087,"humidityMax":86,"precipitationAmountByType":[],"cloudCoverLowAltPct":14,"visibilityMax":34697.878906,"cloudCover":27,"precipitationChance":0,"temperatureMin":5.98207,"cloudCoverMidAltPct":16,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":2,"humidityMin":56,"visibilityMin":29169.382812,"precipitationType":"CLEAR","windSpeedMax":10.368},"precipitationType":"RAIN","sunriseNautical":1761301058,"sunsetCivil":1761345022,"windSpeedMax":14.552842},{"moonrise":1761406274,"moonset":1761437342,"sunrise":1761391124,"sunset":1761429658,"forecastEnd":1761451200,"sunriseCivil":1761389443,"snowfallAmount":0.0,"temperatureMax":14.097731,"solarMidnight":1761367217,"windSpeedAvg":6.75635,"temperatureMaxTime":1761419435,"windGustSpeedMax":20.451601,"precipitationAmount":0.0,"sunriseAstronomical":1761385617,"forecastStart":1761364800,"moonPhase":"WAXING_CRESCENT","humidityMax":86,"precipitationAmountByType":[],"sunsetAstronomical":1761435167,"visibilityMax":34916.546875,"precipitationChance":0,"temperatureMin":5.98207,"maxUvIndex":3,"temperatureMinTime":1761389143,"sunsetNautical":1761433258,"daytimeForecast":{"humidity":61,"daylight":true,"forecastEnd":1761433200,"snowfallAmount":0.0,"temperatureMax":14.097731,"uvIndexMin":0,"temperatureApparentMin":4.790674,"windGustSpeedMax":16.968874,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":13.112373,"precipitationAmount":0.0,"forecastStart":1761390000,"windDirection":291,"windSpeed":7.593,"humidityMax":86,"precipitationAmountByType":[],"cloudCoverLowAltPct":20,"visibilityMax":34916.546875,"cloudCover":72,"precipitationChance":0,"temperatureMin":6.0,"cloudCoverMidAltPct":43,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":52,"humidityMin":48,"visibilityMin":28352.904297,"precipitationType":"CLEAR","windSpeedMax":8.575529},"conditionCode":"MOSTLY_CLOUDY","humidityMin":48,"solarNoon":1761410391,"visibilityMin":28352.904297,"overnightForecast":{"humidity":73,"daylight":false,"forecastEnd":1761476400,"snowfallAmount":0.0,"temperatureMax":12.12,"uvIndexMin":0,"temperatureApparentMin":6.938146,"windGustSpeedMax":16.372353,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":11.713829,"precipitationAmount":0.0,"forecastStart":1761433200,"windDirection":296,"windSpeed":3.83055,"humidityMax":81,"precipitationAmountByType":[],"cloudCoverLowAltPct":14,"visibilityMax":32246.925781,"cloudCover":75,"precipitationChance":0,"temperatureMin":6.873954,"cloudCoverMidAltPct":59,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":96,"humidityMin":58,"visibilityMin":25157.0,"precipitationType":"CLEAR","windSpeedMax":5.1264},"precipitationType":"RAIN","sunriseNautical":1761387521,"sunsetCivil":1761431344,"windSpeedMax":8.575529},{"moonrise":1761495938,"moonset":1761526859,"sunrise":1761477593,"sunset":1761515978,"forecastEnd":1761537600,"sunriseCivil":1761475908,"snowfallAmount":0.0,"temperatureMax":13.757895,"solarMidnight":1761453610,"windSpeedAvg":5.215644,"temperatureMaxTime":1761504319,"windGustSpeedMax":13.48378,"precipitationAmount":0.0,"sunriseAstronomical":1761472079,"forecastStart":1761451200,"moonPhase":"WAXING_CRESCENT","humidityMax":85,"precipitationAmountByType":[],"sunsetAstronomical":1761521493,"visibilityMax":35069.429688,"precipitationChance":0,"temperatureMin":6.873954,"maxUvIndex":3,"temperatureMinTime":1761473869,"sunsetNautical":1761519583,"daytimeForecast":{"humidity":65,"daylight":true,"forecastEnd":1761519600,"snowfallAmount":0.0,"temperatureMax":13.757895,"uvIndexMin":0,"temperatureApparentMin":6.960814,"windGustSpeedMax":13.48378,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":13.043096,"precipitationAmount":0.0,"forecastStart":1761476400,"windDirection":188,"windSpeed":5.7639,"humidityMax":81,"precipitationAmountByType":[],"cloudCoverLowAltPct":39,"visibilityMax":31860.765625,"cloudCover":81,"precipitationChance":0,"temperatureMin":6.94,"cloudCoverMidAltPct":79,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":60,"humidityMin":56,"visibilityMin":24641.685547,"precipitationType":"CLEAR","windSpeedMax":6.216825},"conditionCode":"MOSTLY_CLOUDY","humidityMin":56,"solarNoon":1761496785,"visibilityMin":24641.685547,"overnightForecast":{"humidity":85,"daylight":false,"forecastEnd":1761562800,"snowfallAmount":0.0,"temperatureMax":11.81,"uvIndexMin":0,"temperatureApparentMin":7.008282,"windGustSpeedMax":10.8612,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":11.379537,"precipitationAmount":0.0,"forecastStart":1761519600,"windDirection":357,"windSpeed":7.151888,"humidityMax":92,"precipitationAmountByType":[],"cloudCoverLowAltPct":56,"visibilityMax":35069.429688,"cloudCover":77,"precipitationChance":0,"temperatureMin":8.639686,"cloudCoverMidAltPct":84,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":18,"humidityMin":67,"visibilityMin":28417.804688,"precipitationType":"CLEAR","windSpeedMax":10.1268},"precipitationType":"RAIN","sunriseNautical":1761473984,"sunsetCivil":1761517667,"windSpeedMax":6.216825},{"moonrise":1761585176,"moonset":1761616820,"sunrise":1761564062,"sunset":1761602300,"forecastEnd":1761624000,"sunriseCivil":1761562373,"snowfallAmount":0.0,"temperatureMax":16.050341,"solarMidnight":1761540004,"windSpeedAvg":9.453151,"temperatureMaxTime":1761591724,"windGustSpeedMax":20.173061,"precipitationAmount":0.0,"sunriseAstronomical":1761558540,"forecastStart":1761537600,"moonPhase":"WAXING_CRESCENT","humidityMax":92,"precipitationAmountByType":[],"sunsetAstronomical":1761607821,"visibilityMax":36322.265625,"precipitationChance":0,"temperatureMin":8.639686,"maxUvIndex":3,"temperatureMinTime":1761555828,"sunsetNautical":1761605911,"daytimeForecast":{"humidity":70,"daylight":true,"forecastEnd":1761606000,"snowfallAmount":0.0,"temperatureMax":16.050341,"uvIndexMin":0,"temperatureApparentMin":7.033569,"windGustSpeedMax":19.7316,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":15.328562,"precipitationAmount":0.0,"forecastStart":1761562800,"windDirection":54,"windSpeed":9.641274,"humidityMax":90,"precipitationAmountByType":[],"cloudCoverLowAltPct":48,"visibilityMax":36322.265625,"cloudCover":69,"precipitationChance":0,"temperatureMin":8.88,"cloudCoverMidAltPct":39,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":9,"humidityMin":60,"visibilityMin":28688.398438,"precipitationType":"CLEAR","windSpeedMax":10.742399},"conditionCode":"MOSTLY_CLOUDY","humidityMin":60,"solarNoon":1761583180,"visibilityMin":28688.398438,"overnightForecast":{"humidity":84,"daylight":false,"forecastEnd":1761649200,"snowfallAmount":0.0,"temperatureMax":13.64,"uvIndexMin":0,"temperatureApparentMin":3.542812,"windGustSpeedMax":20.173061,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":11.504132,"precipitationAmount":0.0,"forecastStart":1761606000,"windDirection":33,"windSpeed":10.611563,"humidityMax":94,"precipitationAmountByType":[],"cloudCoverLowAltPct":14,"visibilityMax":34023.632812,"cloudCover":41,"precipitationChance":0,"temperatureMin":6.501806,"cloudCoverMidAltPct":3,"precipitationIntensityMax":0.0,"conditionCode":"PARTLY_CLOUDY","cloudCoverHighAltPct":0,"humidityMin":70,"visibilityMin":25308.0,"precipitationType":"CLEAR","windSpeedMax":11.312479},"precipitationType":"RAIN","sunriseNautical":1761560447,"sunsetCivil":1761603991,"windSpeedMax":11.312479},{"moonrise":1761673972,"moonset":1761707103,"sunrise":1761650531,"sunset":1761688623,"forecastEnd":1761710400,"sunriseCivil":1761648838,"snowfallAmount":0.0,"temperatureMax":16.220354,"solarMidnight":1761626399,"windSpeedAvg":10.950863,"temperatureMaxTime":1761677832,"windGustSpeedMax":31.8384,"precipitationAmount":0.0,"sunriseAstronomical":1761645002,"forecastStart":1761624000,"moonPhase":"WAXING_CRESCENT","humidityMax":94,"precipitationAmountByType":[],"sunsetAstronomical":1761694150,"visibilityMax":35435.453125,"precipitationChance":0,"temperatureMin":6.501806,"maxUvIndex":4,"temperatureMinTime":1761648693,"sunsetNautical":1761692239,"daytimeForecast":{"humidity":69,"daylight":true,"forecastEnd":1761692400,"snowfallAmount":0.0,"temperatureMax":16.220354,"uvIndexMin":0,"temperatureApparentMin":3.612268,"windGustSpeedMax":28.4904,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":4,"temperatureApparentMax":16.101068,"precipitationAmount":0.0,"forecastStart":1761649200,"windDirection":65,"windSpeed":10.691,"humidityMax":94,"precipitationAmountByType":[],"cloudCoverLowAltPct":17,"visibilityMax":35435.453125,"cloudCover":33,"precipitationChance":0,"temperatureMin":6.51,"cloudCoverMidAltPct":16,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":6,"humidityMin":56,"visibilityMin":16905.0,"precipitationType":"CLEAR","windSpeedMax":11.379614},"conditionCode":"MOSTLY_CLEAR","humidityMin":56,"solarNoon":1761669575,"visibilityMin":14173.703125,"overnightForecast":{"humidity":85,"daylight":false,"forecastEnd":1761735600,"snowfallAmount":0.0,"temperatureMax":13.19,"uvIndexMin":0,"temperatureApparentMin":4.16664,"windGustSpeedMax":38.610031,"perceivedPrecipitationIntensityMax":0.804,"uvIndexMax":0,"temperatureApparentMax":10.807237,"precipitationAmount":2.414,"forecastStart":1761692400,"windDirection":90,"windSpeed":12.916863,"humidityMax":94,"precipitationAmountByType":[{"expected":2.414,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"cloudCoverLowAltPct":88,"visibilityMax":20582.857422,"cloudCover":64,"precipitationChance":24,"temperatureMin":7.55,"cloudCoverMidAltPct":93,"precipitationIntensityMax":0.541758,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":43,"humidityMin":69,"visibilityMin":13093.134766,"precipitationType":"RAIN","windSpeedMax":14.274548},"precipitationType":"RAIN","sunriseNautical":1761646910,"sunsetCivil":1761690316,"windSpeedMax":13.402707},{"moonrise":1761762382,"sunrise":1761737000,"sunset":1761774947,"forecastEnd":1761796800,"sunriseCivil":1761735303,"snowfallAmount":0.0,"temperatureMax":16.245462,"solarMidnight":1761712795,"windSpeedAvg":16.424599,"temperatureMaxTime":1761767437,"windGustSpeedMax":50.617004,"precipitationAmount":7.737,"sunriseAstronomical":1761731463,"forecastStart":1761710400,"moonPhase":"FIRST_QUARTER","humidityMax":94,"precipitationAmountByType":[{"expected":7.737,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"sunsetAstronomical":1761780481,"visibilityMax":24203.0,"precipitationChance":51,"temperatureMin":4.73919,"maxUvIndex":3,"temperatureMinTime":1761742843,"sunsetNautical":1761778569,"daytimeForecast":{"humidity":74,"daylight":true,"forecastEnd":1761778800,"snowfallAmount":0.0,"temperatureMax":16.245462,"uvIndexMin":0,"temperatureApparentMin":2.770287,"windGustSpeedMax":50.616001,"perceivedPrecipitationIntensityMax":0.8,"uvIndexMax":3,"temperatureApparentMax":14.084887,"precipitationAmount":5.039,"forecastStart":1761735600,"windDirection":87,"windSpeed":15.368976,"humidityMax":94,"precipitationAmountByType":[{"expected":5.039,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"cloudCoverLowAltPct":100,"visibilityMax":22176.816406,"cloudCover":48,"precipitationChance":39,"temperatureMin":4.73919,"cloudCoverMidAltPct":48,"precipitationIntensityMax":0.536092,"conditionCode":"DRIZZLE","cloudCoverHighAltPct":0,"humidityMin":63,"visibilityMin":11223.716797,"precipitationType":"RAIN","windSpeedMax":17.1576},"conditionCode":"DRIZZLE","humidityMin":63,"solarNoon":1761755971,"visibilityMin":11223.716797,"overnightForecast":{"humidity":84,"daylight":false,"forecastEnd":1761822000,"snowfallAmount":0.0,"temperatureMax":14.52,"uvIndexMin":0,"temperatureApparentMin":0.034572,"windGustSpeedMax":50.617004,"perceivedPrecipitationIntensityMax":0.601,"uvIndexMax":0,"temperatureApparentMax":10.488429,"precipitationAmount":0.284,"forecastStart":1761778800,"windDirection":356,"windSpeed":17.016912,"humidityMax":88,"precipitationAmountByType":[{"expected":0.284,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"cloudCoverLowAltPct":47,"visibilityMax":24203.740234,"cloudCover":40,"precipitationChance":24,"temperatureMin":5.428105,"cloudCoverMidAltPct":0,"precipitationIntensityMax":0.284,"conditionCode":"PARTLY_CLOUDY","cloudCoverHighAltPct":0,"humidityMin":74,"visibilityMin":22040.0,"precipitationType":"RAIN","windSpeedMax":27.57523},"precipitationType":"RAIN","sunriseNautical":1761733373,"sunsetCivil":1761776643,"windSpeedMax":27.57523}]} """ static let hourlyWeatherJSON = """ - {"name":"HourlyForecast","metadata":{"attributionURL":"https://weatherkit.apple.com/legal-attribution.html","expireTime":"2022-11-07T16:14:51Z","latitude":37.523,"longitude":-77.573,"readTime":"2022-11-07T15:14:51Z","reportedTime":"2022-11-07T14:00:00Z","units":"m","version":1},"hours":[{"forecastStart":"2022-11-07T03:00:00Z","cloudCover":0.14,"conditionCode":"MostlyClear","daylight":false,"humidity":0.81,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.71,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.82,"temperatureApparent":21.55,"temperatureDewPoint":17.36,"uvIndex":0,"visibility":27292.53,"windDirection":188,"windGust":9.03,"windSpeed":4.16},{"forecastStart":"2022-11-07T04:00:00Z","cloudCover":0.08,"conditionCode":"Clear","daylight":false,"humidity":0.81,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.70,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.78,"temperatureApparent":21.51,"temperatureDewPoint":17.37,"uvIndex":0,"visibility":25567.26,"windDirection":189,"windGust":9.22,"windSpeed":4.55},{"forecastStart":"2022-11-07T05:00:00Z","cloudCover":0.04,"conditionCode":"Clear","daylight":false,"humidity":0.83,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.80,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.40,"temperatureApparent":21.12,"temperatureDewPoint":17.38,"uvIndex":0,"visibility":24472.38,"windDirection":195,"windGust":7.41,"windSpeed":3.73},{"forecastStart":"2022-11-07T06:00:00Z","cloudCover":0.05,"conditionCode":"Clear","daylight":false,"humidity":0.83,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.88,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.61,"temperatureApparent":21.42,"temperatureDewPoint":17.69,"uvIndex":0,"visibility":23794.55,"windDirection":202,"windGust":8.67,"windSpeed":4.55},{"forecastStart":"2022-11-07T07:00:00Z","cloudCover":0.08,"conditionCode":"Clear","daylight":false,"humidity":0.84,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.89,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.14,"temperatureApparent":20.87,"temperatureDewPoint":17.44,"uvIndex":0,"visibility":23545.82,"windDirection":203,"windGust":7.64,"windSpeed":4.32},{"forecastStart":"2022-11-07T08:00:00Z","cloudCover":0.08,"conditionCode":"Clear","daylight":false,"humidity":0.87,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.77,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.40,"temperatureApparent":20.03,"temperatureDewPoint":17.10,"uvIndex":0,"visibility":22750.51,"windDirection":219,"windGust":6.48,"windSpeed":2.93},{"forecastStart":"2022-11-07T09:00:00Z","cloudCover":0.08,"conditionCode":"Clear","daylight":false,"humidity":0.86,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.75,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.04,"temperatureApparent":19.58,"temperatureDewPoint":16.73,"uvIndex":0,"visibility":23824.20,"windDirection":219,"windGust":6.06,"windSpeed":2.86},{"forecastStart":"2022-11-07T10:00:00Z","cloudCover":0.10,"conditionCode":"Clear","daylight":false,"humidity":0.88,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.92,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":18.34,"temperatureApparent":18.78,"temperatureDewPoint":16.31,"uvIndex":0,"visibility":22650.84,"windDirection":206,"windGust":5.43,"windSpeed":2.67},{"forecastStart":"2022-11-07T11:00:00Z","cloudCover":0.09,"conditionCode":"Clear","daylight":false,"humidity":0.90,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1023.21,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":17.51,"temperatureApparent":17.83,"temperatureDewPoint":15.83,"uvIndex":0,"visibility":21389.10,"windDirection":224,"windGust":3.51,"windSpeed":0.85},{"forecastStart":"2022-11-07T12:00:00Z","cloudCover":0.09,"conditionCode":"Clear","daylight":true,"humidity":0.91,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1023.64,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":16.89,"temperatureApparent":17.13,"temperatureDewPoint":15.38,"uvIndex":0,"visibility":19637.21,"windDirection":261,"windGust":2.98,"windSpeed":1.04},{"forecastStart":"2022-11-07T13:00:00Z","cloudCover":0.05,"conditionCode":"Clear","daylight":true,"humidity":0.90,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1023.92,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":18.08,"temperatureApparent":18.54,"temperatureDewPoint":16.42,"uvIndex":1,"visibility":22752.03,"windDirection":238,"windGust":3.49,"windSpeed":3.36},{"forecastStart":"2022-11-07T14:00:00Z","cloudCover":0.04,"conditionCode":"Clear","daylight":true,"humidity":0.82,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1024.35,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.67,"temperatureApparent":21.45,"temperatureDewPoint":17.58,"uvIndex":2,"visibility":26356.60,"windDirection":309,"windGust":15.60,"windSpeed":6.05},{"forecastStart":"2022-11-07T15:00:00Z","cloudCover":0.06,"conditionCode":"Clear","daylight":true,"humidity":0.76,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1024.50,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":22.88,"temperatureApparent":23.94,"temperatureDewPoint":18.36,"uvIndex":3,"visibility":28696.28,"windDirection":355,"windGust":19.05,"windSpeed":8.03},{"forecastStart":"2022-11-07T16:00:00Z","cloudCover":0.12,"conditionCode":"Clear","daylight":true,"humidity":0.67,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1024.34,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":24.46,"temperatureApparent":25.47,"temperatureDewPoint":17.95,"uvIndex":4,"visibility":31672.55,"windDirection":12,"windGust":20.63,"windSpeed":10.13},{"forecastStart":"2022-11-07T17:00:00Z","cloudCover":0.11,"conditionCode":"Clear","daylight":true,"humidity":0.59,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1023.77,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":25.70,"temperatureApparent":26.47,"temperatureDewPoint":17.00,"uvIndex":4,"visibility":33754.40,"windDirection":14,"windGust":20.45,"windSpeed":9.91},{"forecastStart":"2022-11-07T18:00:00Z","cloudCover":0.21,"conditionCode":"MostlyClear","daylight":true,"humidity":0.48,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1023.32,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":26.36,"temperatureApparent":26.46,"temperatureDewPoint":14.40,"uvIndex":4,"visibility":42923.40,"windDirection":8,"windGust":21.91,"windSpeed":10.56},{"forecastStart":"2022-11-07T19:00:00Z","cloudCover":0.16,"conditionCode":"MostlyClear","daylight":true,"humidity":0.42,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1023.17,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":26.54,"temperatureApparent":26.25,"temperatureDewPoint":12.72,"uvIndex":3,"visibility":45956.55,"windDirection":12,"windGust":21.52,"windSpeed":10.22},{"forecastStart":"2022-11-07T20:00:00Z","cloudCover":0.16,"conditionCode":"MostlyClear","daylight":true,"humidity":0.41,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1023.31,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":26.10,"temperatureApparent":25.61,"temperatureDewPoint":11.67,"uvIndex":1,"visibility":46553.77,"windDirection":17,"windGust":20.75,"windSpeed":10.06},{"forecastStart":"2022-11-07T21:00:00Z","cloudCover":0.16,"conditionCode":"MostlyClear","daylight":true,"humidity":0.43,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1023.64,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":25.01,"temperatureApparent":24.53,"temperatureDewPoint":11.63,"uvIndex":0,"visibility":45869.30,"windDirection":21,"windGust":19.80,"windSpeed":9.91},{"forecastStart":"2022-11-07T22:00:00Z","cloudCover":0.12,"conditionCode":"Clear","daylight":true,"humidity":0.46,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1024.18,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":22.80,"temperatureApparent":22.21,"temperatureDewPoint":10.70,"uvIndex":0,"visibility":42891.39,"windDirection":27,"windGust":20.33,"windSpeed":9.64},{"forecastStart":"2022-11-07T23:00:00Z","cloudCover":0.07,"conditionCode":"Clear","daylight":false,"humidity":0.49,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1025.05,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.84,"temperatureApparent":20.16,"temperatureDewPoint":9.79,"uvIndex":0,"visibility":41491.08,"windDirection":30,"windGust":23.14,"windSpeed":9.57},{"forecastStart":"2022-11-08T00:00:00Z","cloudCover":0.07,"conditionCode":"Clear","daylight":false,"humidity":0.53,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1025.79,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.29,"temperatureApparent":18.63,"temperatureDewPoint":9.61,"uvIndex":0,"visibility":40032.68,"windDirection":28,"windGust":25.63,"windSpeed":9.79},{"forecastStart":"2022-11-08T01:00:00Z","cloudCover":0.01,"conditionCode":"Clear","daylight":false,"humidity":0.57,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1026.38,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":18.07,"temperatureApparent":17.43,"temperatureDewPoint":9.55,"uvIndex":0,"visibility":38379.76,"windDirection":18,"windGust":27.15,"windSpeed":10.52},{"forecastStart":"2022-11-08T02:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.61,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1026.79,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":16.97,"temperatureApparent":16.32,"temperatureDewPoint":9.27,"uvIndex":0,"visibility":37173.65,"windDirection":14,"windGust":27.87,"windSpeed":10.66},{"forecastStart":"2022-11-08T03:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.59,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.33,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":16.11,"temperatureApparent":15.36,"temperatureDewPoint":8.12,"uvIndex":0,"visibility":39416.25,"windDirection":10,"windGust":31.85,"windSpeed":12.38},{"forecastStart":"2022-11-08T04:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.54,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.68,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":15.24,"temperatureApparent":14.34,"temperatureDewPoint":6.07,"uvIndex":0,"visibility":42938.47,"windDirection":9,"windGust":35.35,"windSpeed":14.27},{"forecastStart":"2022-11-08T05:00:00Z","cloudCover":0.01,"conditionCode":"Clear","daylight":false,"humidity":0.51,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.95,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.25,"temperatureApparent":13.25,"temperatureDewPoint":4.25,"uvIndex":0,"visibility":44686.10,"windDirection":9,"windGust":37.07,"windSpeed":15.71},{"forecastStart":"2022-11-08T06:00:00Z","cloudCover":0.06,"conditionCode":"Clear","daylight":false,"humidity":0.49,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1028.41,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":13.41,"temperatureApparent":12.37,"temperatureDewPoint":3.02,"uvIndex":0,"visibility":44115.04,"windDirection":11,"windGust":40.24,"windSpeed":17.17},{"forecastStart":"2022-11-08T07:00:00Z","cloudCover":0.14,"conditionCode":"MostlyClear","daylight":false,"humidity":0.51,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1029.01,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":12.50,"temperatureApparent":11.46,"temperatureDewPoint":2.50,"uvIndex":0,"visibility":36475.13,"windDirection":10,"windGust":42.68,"windSpeed":17.43},{"forecastStart":"2022-11-08T08:00:00Z","cloudCover":0.11,"conditionCode":"Clear","daylight":false,"humidity":0.52,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1029.43,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":11.32,"temperatureApparent":10.28,"temperatureDewPoint":1.70,"uvIndex":0,"visibility":36511.58,"windDirection":10,"windGust":43.20,"windSpeed":18.13},{"forecastStart":"2022-11-08T09:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.54,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1030.00,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.24,"temperatureApparent":9.23,"temperatureDewPoint":1.24,"uvIndex":0,"visibility":36451.05,"windDirection":9,"windGust":42.92,"windSpeed":17.95},{"forecastStart":"2022-11-08T10:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.56,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1030.84,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.29,"temperatureApparent":6.73,"temperatureDewPoint":0.99,"uvIndex":0,"visibility":36395.62,"windDirection":9,"windGust":42.68,"windSpeed":17.60},{"forecastStart":"2022-11-08T11:00:00Z","cloudCover":0.10,"conditionCode":"Clear","daylight":false,"humidity":0.59,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.60,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.50,"temperatureApparent":5.76,"temperatureDewPoint":0.92,"uvIndex":0,"visibility":34395.05,"windDirection":9,"windGust":42.77,"windSpeed":17.43},{"forecastStart":"2022-11-08T12:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":true,"humidity":0.61,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1032.38,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":7.98,"temperatureApparent":5.15,"temperatureDewPoint":0.98,"uvIndex":0,"visibility":34218.55,"windDirection":10,"windGust":42.00,"windSpeed":17.09},{"forecastStart":"2022-11-08T13:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":true,"humidity":0.61,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.30,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.73,"temperatureApparent":6.02,"temperatureDewPoint":1.57,"uvIndex":0,"visibility":34064.63,"windDirection":12,"windGust":42.30,"windSpeed":17.70},{"forecastStart":"2022-11-08T14:00:00Z","cloudCover":0.03,"conditionCode":"Clear","daylight":true,"humidity":0.58,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.83,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.05,"temperatureApparent":9.08,"temperatureDewPoint":2.13,"uvIndex":2,"visibility":34067.82,"windDirection":15,"windGust":42.01,"windSpeed":18.99},{"forecastStart":"2022-11-08T15:00:00Z","cloudCover":0.05,"conditionCode":"Clear","daylight":true,"humidity":0.54,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.96,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":11.65,"temperatureApparent":10.66,"temperatureDewPoint":2.65,"uvIndex":3,"visibility":34224.12,"windDirection":18,"windGust":39.68,"windSpeed":19.07},{"forecastStart":"2022-11-08T16:00:00Z","cloudCover":0.01,"conditionCode":"Clear","daylight":true,"humidity":0.50,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.65,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":13.10,"temperatureApparent":12.05,"temperatureDewPoint":2.76,"uvIndex":4,"visibility":34605.53,"windDirection":21,"windGust":37.57,"windSpeed":18.66},{"forecastStart":"2022-11-08T17:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":true,"humidity":0.46,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1032.85,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.44,"temperatureApparent":13.35,"temperatureDewPoint":2.89,"uvIndex":4,"visibility":34894.75,"windDirection":22,"windGust":36.93,"windSpeed":18.00},{"forecastStart":"2022-11-08T18:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":true,"humidity":0.43,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1032.15,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":15.37,"temperatureApparent":14.23,"temperatureDewPoint":2.81,"uvIndex":4,"visibility":36643.48,"windDirection":24,"windGust":35.95,"windSpeed":17.40},{"forecastStart":"2022-11-08T19:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":true,"humidity":0.41,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.76,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":15.87,"temperatureApparent":14.70,"temperatureDewPoint":2.67,"uvIndex":3,"visibility":36983.55,"windDirection":27,"windGust":35.24,"windSpeed":17.18},{"forecastStart":"2022-11-08T20:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":true,"humidity":0.41,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.72,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":15.64,"temperatureApparent":14.47,"temperatureDewPoint":2.46,"uvIndex":1,"visibility":36994.67,"windDirection":28,"windGust":34.62,"windSpeed":16.82},{"forecastStart":"2022-11-08T21:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":true,"humidity":0.43,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.81,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.84,"temperatureApparent":13.70,"temperatureDewPoint":2.41,"uvIndex":0,"visibility":36573.15,"windDirection":31,"windGust":33.43,"windSpeed":15.72},{"forecastStart":"2022-11-08T22:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":true,"humidity":0.48,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1032.09,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":13.31,"temperatureApparent":12.24,"temperatureDewPoint":2.41,"uvIndex":0,"visibility":34992.36,"windDirection":33,"windGust":31.33,"windSpeed":13.52},{"forecastStart":"2022-11-08T23:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.53,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1032.68,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":11.85,"temperatureApparent":10.83,"temperatureDewPoint":2.52,"uvIndex":0,"visibility":34867.11,"windDirection":33,"windGust":31.92,"windSpeed":12.00},{"forecastStart":"2022-11-09T00:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.57,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.23,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.60,"temperatureApparent":9.63,"temperatureDewPoint":2.34,"uvIndex":0,"visibility":34140.88,"windDirection":33,"windGust":32.44,"windSpeed":11.66},{"forecastStart":"2022-11-09T01:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.60,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.51,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.86,"temperatureApparent":8.17,"temperatureDewPoint":2.41,"uvIndex":0,"visibility":33977.88,"windDirection":28,"windGust":32.17,"windSpeed":11.80},{"forecastStart":"2022-11-09T02:00:00Z","cloudCover":0.01,"conditionCode":"Clear","daylight":false,"humidity":0.63,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.72,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.23,"temperatureApparent":7.42,"temperatureDewPoint":2.62,"uvIndex":0,"visibility":33756.81,"windDirection":24,"windGust":31.69,"windSpeed":11.76},{"forecastStart":"2022-11-09T03:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.66,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.85,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.62,"temperatureApparent":6.67,"temperatureDewPoint":2.71,"uvIndex":0,"visibility":33790.84,"windDirection":20,"windGust":31.13,"windSpeed":11.82},{"forecastStart":"2022-11-09T04:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.69,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.72,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.02,"temperatureApparent":5.96,"temperatureDewPoint":2.74,"uvIndex":0,"visibility":33646.87,"windDirection":17,"windGust":30.28,"windSpeed":11.70},{"forecastStart":"2022-11-09T05:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.72,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.64,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":7.46,"temperatureApparent":5.27,"temperatureDewPoint":2.68,"uvIndex":0,"visibility":32875.18,"windDirection":17,"windGust":30.54,"windSpeed":11.77},{"forecastStart":"2022-11-09T06:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.74,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.65,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":6.94,"temperatureApparent":4.70,"temperatureDewPoint":2.53,"uvIndex":0,"visibility":32802.07,"windDirection":15,"windGust":29.62,"windSpeed":11.48},{"forecastStart":"2022-11-09T07:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.75,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.74,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":6.36,"temperatureApparent":3.94,"temperatureDewPoint":2.25,"uvIndex":0,"visibility":32769.63,"windDirection":14,"windGust":29.62,"windSpeed":11.79},{"forecastStart":"2022-11-09T08:00:00Z","cloudCover":0.01,"conditionCode":"Clear","daylight":false,"humidity":0.76,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.66,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.96,"temperatureApparent":3.36,"temperatureDewPoint":2.03,"uvIndex":0,"visibility":32691.35,"windDirection":14,"windGust":30.29,"windSpeed":12.29},{"forecastStart":"2022-11-09T09:00:00Z","cloudCover":0.06,"conditionCode":"Clear","daylight":false,"humidity":0.77,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.71,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.60,"temperatureApparent":2.92,"temperatureDewPoint":1.80,"uvIndex":0,"visibility":32696.49,"windDirection":15,"windGust":31.06,"windSpeed":12.34},{"forecastStart":"2022-11-09T10:00:00Z","cloudCover":0.08,"conditionCode":"Clear","daylight":false,"humidity":0.77,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1034.02,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.25,"temperatureApparent":2.46,"temperatureDewPoint":1.56,"uvIndex":0,"visibility":32550.78,"windDirection":14,"windGust":31.43,"windSpeed":12.52},{"forecastStart":"2022-11-09T11:00:00Z","cloudCover":0.04,"conditionCode":"Clear","daylight":false,"humidity":0.77,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1034.38,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":4.99,"temperatureApparent":2.11,"temperatureDewPoint":1.31,"uvIndex":0,"visibility":31875.18,"windDirection":14,"windGust":30.80,"windSpeed":12.72},{"forecastStart":"2022-11-09T12:00:00Z","cloudCover":0.07,"conditionCode":"Clear","daylight":true,"humidity":0.77,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1034.83,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.17,"temperatureApparent":2.31,"temperatureDewPoint":1.45,"uvIndex":0,"visibility":31967.78,"windDirection":12,"windGust":30.38,"windSpeed":12.83},{"forecastStart":"2022-11-09T13:00:00Z","cloudCover":0.08,"conditionCode":"Clear","daylight":true,"humidity":0.72,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1035.30,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":6.63,"temperatureApparent":4.09,"temperatureDewPoint":1.99,"uvIndex":0,"visibility":33063.94,"windDirection":14,"windGust":29.70,"windSpeed":12.80},{"forecastStart":"2022-11-09T14:00:00Z","cloudCover":0.11,"conditionCode":"Clear","daylight":true,"humidity":0.66,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1035.47,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.84,"temperatureApparent":6.68,"temperatureDewPoint":2.79,"uvIndex":1,"visibility":33434.08,"windDirection":21,"windGust":29.06,"windSpeed":13.53},{"forecastStart":"2022-11-09T15:00:00Z","cloudCover":0.13,"conditionCode":"MostlyClear","daylight":true,"humidity":0.61,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1035.31,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":11.02,"temperatureApparent":10.11,"temperatureDewPoint":3.81,"uvIndex":3,"visibility":34166.43,"windDirection":27,"windGust":27.75,"windSpeed":13.37},{"forecastStart":"2022-11-09T16:00:00Z","cloudCover":0.22,"conditionCode":"MostlyClear","daylight":true,"humidity":0.58,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1034.84,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":13.16,"temperatureApparent":12.26,"temperatureDewPoint":5.06,"uvIndex":4,"visibility":33503.16,"windDirection":35,"windGust":26.94,"windSpeed":13.02},{"forecastStart":"2022-11-09T17:00:00Z","cloudCover":0.26,"conditionCode":"MostlyClear","daylight":true,"humidity":0.55,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.89,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.75,"temperatureApparent":13.85,"temperatureDewPoint":5.87,"uvIndex":4,"visibility":32523.49,"windDirection":42,"windGust":26.00,"windSpeed":12.46},{"forecastStart":"2022-11-09T18:00:00Z","cloudCover":0.29,"conditionCode":"MostlyClear","daylight":true,"humidity":0.54,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1032.98,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":15.85,"temperatureApparent":14.96,"temperatureDewPoint":6.47,"uvIndex":3,"visibility":32012.60,"windDirection":50,"windGust":24.60,"windSpeed":11.92},{"forecastStart":"2022-11-09T19:00:00Z","cloudCover":0.27,"conditionCode":"MostlyClear","daylight":true,"humidity":0.53,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1032.36,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":16.46,"temperatureApparent":15.59,"temperatureDewPoint":6.93,"uvIndex":2,"visibility":32468.38,"windDirection":59,"windGust":23.12,"windSpeed":12.05},{"forecastStart":"2022-11-09T20:00:00Z","cloudCover":0.32,"conditionCode":"MostlyClear","daylight":true,"humidity":0.55,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.97,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":16.59,"temperatureApparent":15.76,"temperatureDewPoint":7.40,"uvIndex":1,"visibility":33382.72,"windDirection":66,"windGust":21.32,"windSpeed":12.06},{"forecastStart":"2022-11-09T21:00:00Z","cloudCover":0.26,"conditionCode":"MostlyClear","daylight":true,"humidity":0.57,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.72,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":16.00,"temperatureApparent":15.19,"temperatureDewPoint":7.41,"uvIndex":0,"visibility":34051.69,"windDirection":68,"windGust":19.54,"windSpeed":11.42},{"forecastStart":"2022-11-09T22:00:00Z","cloudCover":0.14,"conditionCode":"MostlyClear","daylight":true,"humidity":0.62,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.63,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.42,"temperatureApparent":13.65,"temperatureDewPoint":7.23,"uvIndex":0,"visibility":33495.76,"windDirection":63,"windGust":16.51,"windSpeed":9.59},{"forecastStart":"2022-11-09T23:00:00Z","cloudCover":0.15,"conditionCode":"MostlyClear","daylight":false,"humidity":0.67,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.78,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":12.95,"temperatureApparent":12.20,"temperatureDewPoint":7.06,"uvIndex":0,"visibility":33290.20,"windDirection":57,"windGust":17.79,"windSpeed":9.07},{"forecastStart":"2022-11-10T00:00:00Z","cloudCover":0.21,"conditionCode":"MostlyClear","daylight":false,"humidity":0.72,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.93,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":11.94,"temperatureApparent":11.22,"temperatureDewPoint":6.99,"uvIndex":0,"visibility":32788.98,"windDirection":47,"windGust":17.87,"windSpeed":8.50},{"forecastStart":"2022-11-10T01:00:00Z","cloudCover":0.25,"conditionCode":"MostlyClear","daylight":false,"humidity":0.75,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.82,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":11.13,"temperatureApparent":10.41,"temperatureDewPoint":6.81,"uvIndex":0,"visibility":32788.16,"windDirection":39,"windGust":17.19,"windSpeed":7.98},{"forecastStart":"2022-11-10T02:00:00Z","cloudCover":0.25,"conditionCode":"MostlyClear","daylight":false,"humidity":0.78,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.71,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.45,"temperatureApparent":9.75,"temperatureDewPoint":6.80,"uvIndex":0,"visibility":32347.86,"windDirection":30,"windGust":17.37,"windSpeed":7.91},{"forecastStart":"2022-11-10T03:00:00Z","cloudCover":0.25,"conditionCode":"MostlyClear","daylight":false,"humidity":0.81,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.44,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.63,"temperatureApparent":8.55,"temperatureDewPoint":6.55,"uvIndex":0,"visibility":32295.99,"windDirection":24,"windGust":17.22,"windSpeed":8.08},{"forecastStart":"2022-11-10T04:00:00Z","cloudCover":0.40,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.84,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.05,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.09,"temperatureApparent":7.89,"temperatureDewPoint":6.53,"uvIndex":0,"visibility":31388.40,"windDirection":18,"windGust":16.91,"windSpeed":8.19},{"forecastStart":"2022-11-10T05:00:00Z","cloudCover":0.36,"conditionCode":"MostlyClear","daylight":false,"humidity":0.86,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1030.53,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.70,"temperatureApparent":7.48,"temperatureDewPoint":6.53,"uvIndex":0,"visibility":30507.63,"windDirection":15,"windGust":15.76,"windSpeed":8.03},{"forecastStart":"2022-11-10T06:00:00Z","cloudCover":0.43,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.88,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1030.09,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.37,"temperatureApparent":7.03,"temperatureDewPoint":6.50,"uvIndex":0,"visibility":30164.27,"windDirection":13,"windGust":15.77,"windSpeed":8.25},{"forecastStart":"2022-11-10T07:00:00Z","cloudCover":0.54,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.89,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1029.68,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.11,"temperatureApparent":6.68,"temperatureDewPoint":6.34,"uvIndex":0,"visibility":30109.92,"windDirection":10,"windGust":16.53,"windSpeed":8.49},{"forecastStart":"2022-11-10T08:00:00Z","cloudCover":0.59,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.89,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1029.09,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.39,"temperatureApparent":7.03,"temperatureDewPoint":6.65,"uvIndex":0,"visibility":29956.88,"windDirection":8,"windGust":16.11,"windSpeed":8.39},{"forecastStart":"2022-11-10T09:00:00Z","cloudCover":0.66,"conditionCode":"MostlyCloudy","daylight":false,"humidity":0.89,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1028.62,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.57,"temperatureApparent":7.23,"temperatureDewPoint":6.81,"uvIndex":0,"visibility":29512.61,"windDirection":9,"windGust":15.70,"windSpeed":8.49},{"forecastStart":"2022-11-10T10:00:00Z","cloudCover":0.67,"conditionCode":"MostlyCloudy","daylight":false,"humidity":0.89,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1028.31,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.68,"temperatureApparent":7.40,"temperatureDewPoint":6.94,"uvIndex":0,"visibility":28499.86,"windDirection":9,"windGust":14.71,"windSpeed":8.30},{"forecastStart":"2022-11-10T11:00:00Z","cloudCover":0.68,"conditionCode":"MostlyCloudy","daylight":false,"humidity":0.89,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1028.09,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.81,"temperatureApparent":7.54,"temperatureDewPoint":7.06,"uvIndex":0,"visibility":27249.08,"windDirection":8,"windGust":15.18,"windSpeed":8.30},{"forecastStart":"2022-11-10T12:00:00Z","cloudCover":0.69,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.88,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.94,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.39,"temperatureApparent":8.25,"temperatureDewPoint":7.53,"uvIndex":0,"visibility":26560.96,"windDirection":7,"windGust":14.78,"windSpeed":8.20},{"forecastStart":"2022-11-10T13:00:00Z","cloudCover":0.59,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.84,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.97,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.61,"temperatureApparent":10.00,"temperatureDewPoint":8.00,"uvIndex":0,"visibility":27433.79,"windDirection":14,"windGust":14.70,"windSpeed":8.27},{"forecastStart":"2022-11-10T14:00:00Z","cloudCover":0.66,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.78,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.94,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":12.55,"temperatureApparent":11.96,"temperatureDewPoint":8.79,"uvIndex":1,"visibility":29111.37,"windDirection":20,"windGust":14.32,"windSpeed":8.06},{"forecastStart":"2022-11-10T15:00:00Z","cloudCover":0.57,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.72,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.61,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.42,"temperatureApparent":13.86,"temperatureDewPoint":9.53,"uvIndex":2,"visibility":30898.88,"windDirection":27,"windGust":14.83,"windSpeed":8.14},{"forecastStart":"2022-11-10T16:00:00Z","cloudCover":0.71,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.68,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1026.81,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":16.47,"temperatureApparent":16.00,"temperatureDewPoint":10.62,"uvIndex":3,"visibility":31758.96,"windDirection":38,"windGust":14.98,"windSpeed":7.95},{"forecastStart":"2022-11-10T17:00:00Z","cloudCover":0.83,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.65,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1025.66,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":18.07,"temperatureApparent":17.68,"temperatureDewPoint":11.45,"uvIndex":3,"visibility":33109.60,"windDirection":53,"windGust":15.06,"windSpeed":7.71},{"forecastStart":"2022-11-10T18:00:00Z","cloudCover":0.86,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.63,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1024.55,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.35,"temperatureApparent":19.05,"temperatureDewPoint":12.17,"uvIndex":3,"visibility":33953.52,"windDirection":69,"windGust":16.72,"windSpeed":8.37},{"forecastStart":"2022-11-10T19:00:00Z","cloudCover":0.84,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.62,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1023.70,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.02,"temperatureApparent":19.77,"temperatureDewPoint":12.54,"uvIndex":2,"visibility":34956.49,"windDirection":82,"windGust":16.69,"windSpeed":8.92},{"forecastStart":"2022-11-10T20:00:00Z","cloudCover":0.76,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.63,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1023.08,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.25,"temperatureApparent":20.07,"temperatureDewPoint":12.98,"uvIndex":1,"visibility":34529.98,"windDirection":90,"windGust":15.21,"windSpeed":9.37},{"forecastStart":"2022-11-10T21:00:00Z","cloudCover":0.77,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.65,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.63,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.83,"temperatureApparent":19.68,"temperatureDewPoint":13.13,"uvIndex":0,"visibility":33862.32,"windDirection":95,"windGust":14.72,"windSpeed":9.54},{"forecastStart":"2022-11-10T22:00:00Z","cloudCover":0.83,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.70,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.38,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":18.65,"temperatureApparent":18.49,"temperatureDewPoint":13.02,"uvIndex":0,"visibility":32963.52,"windDirection":91,"windGust":12.50,"windSpeed":8.87},{"forecastStart":"2022-11-10T23:00:00Z","cloudCover":0.93,"conditionCode":"Cloudy","daylight":false,"humidity":0.74,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.30,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":17.49,"temperatureApparent":17.31,"temperatureDewPoint":12.89,"uvIndex":0,"visibility":31823.29,"windDirection":85,"windGust":13.79,"windSpeed":8.32},{"forecastStart":"2022-11-11T00:00:00Z","cloudCover":0.93,"conditionCode":"Cloudy","daylight":false,"humidity":0.79,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1022.12,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":16.50,"temperatureApparent":16.33,"temperatureDewPoint":12.88,"uvIndex":0,"visibility":30658.25,"windDirection":81,"windGust":16.91,"windSpeed":8.14},{"forecastStart":"2022-11-11T01:00:00Z","cloudCover":0.90,"conditionCode":"Cloudy","daylight":false,"humidity":0.82,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1021.91,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":15.73,"temperatureApparent":15.54,"temperatureDewPoint":12.74,"uvIndex":0,"visibility":30032.51,"windDirection":75,"windGust":17.50,"windSpeed":7.49},{"forecastStart":"2022-11-11T02:00:00Z","cloudCover":0.87,"conditionCode":"MostlyCloudy","daylight":false,"humidity":0.85,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1021.57,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":15.28,"temperatureApparent":15.11,"temperatureDewPoint":12.81,"uvIndex":0,"visibility":28793.82,"windDirection":69,"windGust":16.71,"windSpeed":7.34},{"forecastStart":"2022-11-11T03:00:00Z","cloudCover":0.85,"conditionCode":"MostlyCloudy","daylight":false,"humidity":0.88,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1021.18,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.88,"temperatureApparent":14.71,"temperatureDewPoint":12.84,"uvIndex":0,"visibility":27517.20,"windDirection":66,"windGust":14.70,"windSpeed":7.16},{"forecastStart":"2022-11-11T04:00:00Z","cloudCover":0.86,"conditionCode":"MostlyCloudy","daylight":false,"humidity":0.89,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1020.58,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.46,"temperatureApparent":14.27,"temperatureDewPoint":12.67,"uvIndex":0,"visibility":26044.54,"windDirection":63,"windGust":13.67,"windSpeed":7.09},{"forecastStart":"2022-11-11T05:00:00Z","cloudCover":0.89,"conditionCode":"Cloudy","daylight":false,"humidity":0.90,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1019.92,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.17,"temperatureApparent":13.97,"temperatureDewPoint":12.54,"uvIndex":0,"visibility":24531.12,"windDirection":63,"windGust":14.85,"windSpeed":7.24},{"forecastStart":"2022-11-11T06:00:00Z","cloudCover":0.91,"conditionCode":"Cloudy","daylight":false,"humidity":0.91,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1019.23,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.17,"temperatureApparent":13.99,"temperatureDewPoint":12.65,"uvIndex":0,"visibility":23435.57,"windDirection":60,"windGust":16.70,"windSpeed":7.36},{"forecastStart":"2022-11-11T07:00:00Z","cloudCover":0.94,"conditionCode":"Cloudy","daylight":false,"humidity":0.91,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1018.55,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.03,"temperatureApparent":13.84,"temperatureDewPoint":12.64,"uvIndex":0,"visibility":23188.75,"windDirection":62,"windGust":16.27,"windSpeed":7.44},{"forecastStart":"2022-11-11T08:00:00Z","cloudCover":0.97,"conditionCode":"Cloudy","daylight":false,"humidity":0.92,"precipitationAmount":0.5,"precipitationIntensity":0.5,"precipitationChance":0.18,"precipitationType":"rain","pressure":1017.87,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":13.87,"temperatureApparent":13.67,"temperatureDewPoint":12.53,"uvIndex":0,"visibility":23362.21,"windDirection":68,"windGust":15.86,"windSpeed":7.58},{"forecastStart":"2022-11-11T09:00:00Z","cloudCover":0.98,"conditionCode":"Cloudy","daylight":false,"humidity":0.91,"precipitationAmount":0.6,"precipitationIntensity":0.6,"precipitationChance":0.23,"precipitationType":"rain","pressure":1017.29,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.08,"temperatureApparent":13.90,"temperatureDewPoint":12.72,"uvIndex":0,"visibility":23120.95,"windDirection":69,"windGust":13.63,"windSpeed":7.43},{"forecastStart":"2022-11-11T10:00:00Z","cloudCover":0.99,"conditionCode":"Cloudy","daylight":false,"humidity":0.91,"precipitationAmount":0.6,"precipitationIntensity":0.6,"precipitationChance":0.28,"precipitationType":"rain","pressure":1016.71,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.47,"temperatureApparent":14.34,"temperatureDewPoint":13.10,"uvIndex":0,"visibility":22062.68,"windDirection":72,"windGust":13.17,"windSpeed":7.29},{"forecastStart":"2022-11-11T11:00:00Z","cloudCover":0.99,"conditionCode":"Drizzle","daylight":false,"humidity":0.91,"precipitationAmount":0.5,"precipitationIntensity":0.5,"precipitationChance":0.33,"precipitationType":"rain","pressure":1016.20,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":14.63,"temperatureApparent":14.52,"temperatureDewPoint":13.26,"uvIndex":0,"visibility":20582.75,"windDirection":74,"windGust":12.55,"windSpeed":7.40},{"forecastStart":"2022-11-11T12:00:00Z","cloudCover":0.98,"conditionCode":"Drizzle","daylight":true,"humidity":0.91,"precipitationAmount":0.4,"precipitationIntensity":0.4,"precipitationChance":0.37,"precipitationType":"rain","pressure":1015.73,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":15.19,"temperatureApparent":15.15,"temperatureDewPoint":13.75,"uvIndex":0,"visibility":19048.05,"windDirection":78,"windGust":14.06,"windSpeed":7.70},{"forecastStart":"2022-11-11T13:00:00Z","cloudCover":0.98,"conditionCode":"Drizzle","daylight":true,"humidity":0.90,"precipitationAmount":0.8,"precipitationIntensity":0.8,"precipitationChance":0.38,"precipitationType":"rain","pressure":1015.29,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":16.34,"temperatureApparent":16.46,"temperatureDewPoint":14.70,"uvIndex":0,"visibility":12383.72,"windDirection":85,"windGust":19.97,"windSpeed":8.16},{"forecastStart":"2022-11-11T14:00:00Z","cloudCover":0.98,"conditionCode":"Rain","daylight":true,"humidity":0.90,"precipitationAmount":1.8,"precipitationIntensity":1.8,"precipitationChance":0.43,"precipitationType":"rain","pressure":1014.90,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":17.33,"temperatureApparent":17.61,"temperatureDewPoint":15.58,"uvIndex":1,"visibility":9589.72,"windDirection":99,"windGust":24.43,"windSpeed":9.43},{"forecastStart":"2022-11-11T15:00:00Z","cloudCover":0.98,"conditionCode":"Rain","daylight":true,"humidity":0.88,"precipitationAmount":1.2,"precipitationIntensity":1.2,"precipitationChance":0.43,"precipitationType":"rain","pressure":1014.22,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":18.24,"temperatureApparent":18.68,"temperatureDewPoint":16.31,"uvIndex":2,"visibility":6962.62,"windDirection":110,"windGust":26.78,"windSpeed":10.50},{"forecastStart":"2022-11-11T16:00:00Z","cloudCover":0.98,"conditionCode":"Rain","daylight":true,"humidity":0.88,"precipitationAmount":1.5,"precipitationIntensity":1.5,"precipitationChance":0.44,"precipitationType":"rain","pressure":1013.18,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.04,"temperatureApparent":19.63,"temperatureDewPoint":16.95,"uvIndex":3,"visibility":4983.54,"windDirection":118,"windGust":25.52,"windSpeed":10.61},{"forecastStart":"2022-11-11T17:00:00Z","cloudCover":0.98,"conditionCode":"Thunderstorms","daylight":true,"humidity":0.87,"precipitationAmount":2.1,"precipitationIntensity":2.1,"precipitationChance":0.45,"precipitationType":"rain","pressure":1011.84,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.93,"temperatureApparent":20.71,"temperatureDewPoint":17.66,"uvIndex":3,"visibility":2990.39,"windDirection":129,"windGust":29.13,"windSpeed":11.13},{"forecastStart":"2022-11-11T18:00:00Z","cloudCover":0.98,"conditionCode":"Thunderstorms","daylight":true,"humidity":0.86,"precipitationAmount":2.5,"precipitationIntensity":2.5,"precipitationChance":0.45,"precipitationType":"rain","pressure":1010.59,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.55,"temperatureApparent":21.47,"temperatureDewPoint":18.14,"uvIndex":3,"visibility":1756.37,"windDirection":136,"windGust":30.65,"windSpeed":11.21},{"forecastStart":"2022-11-11T19:00:00Z","cloudCover":0.98,"conditionCode":"Thunderstorms","daylight":true,"humidity":0.86,"precipitationAmount":2.5,"precipitationIntensity":2.5,"precipitationChance":0.44,"precipitationType":"rain","pressure":1009.68,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.99,"temperatureApparent":22.02,"temperatureDewPoint":18.52,"uvIndex":2,"visibility":1567.29,"windDirection":138,"windGust":32.58,"windSpeed":11.22},{"forecastStart":"2022-11-11T20:00:00Z","cloudCover":0.98,"conditionCode":"Thunderstorms","daylight":true,"humidity":0.86,"precipitationAmount":2.0,"precipitationIntensity":2.0,"precipitationChance":0.41,"precipitationType":"rain","pressure":1008.99,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":21.30,"temperatureApparent":22.43,"temperatureDewPoint":18.81,"uvIndex":1,"visibility":2129.94,"windDirection":141,"windGust":32.09,"windSpeed":11.15},{"forecastStart":"2022-11-11T21:00:00Z","cloudCover":0.98,"conditionCode":"Rain","daylight":true,"humidity":0.86,"precipitationAmount":1.5,"precipitationIntensity":1.5,"precipitationChance":0.39,"precipitationType":"rain","pressure":1008.39,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":21.48,"temperatureApparent":22.69,"temperatureDewPoint":19.04,"uvIndex":0,"visibility":3091.70,"windDirection":140,"windGust":30.62,"windSpeed":10.43},{"forecastStart":"2022-11-11T22:00:00Z","cloudCover":0.98,"conditionCode":"Rain","daylight":false,"humidity":0.87,"precipitationAmount":1.1,"precipitationIntensity":1.1,"precipitationChance":0.38,"precipitationType":"rain","pressure":1007.85,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":21.15,"temperatureApparent":22.29,"temperatureDewPoint":18.87,"uvIndex":0,"visibility":4307.36,"windDirection":138,"windGust":27.69,"windSpeed":10.34},{"forecastStart":"2022-11-11T23:00:00Z","cloudCover":0.98,"conditionCode":"Drizzle","daylight":false,"humidity":0.88,"precipitationAmount":0.6,"precipitationIntensity":0.6,"precipitationChance":0.38,"precipitationType":"rain","pressure":1007.45,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.75,"temperatureApparent":21.82,"temperatureDewPoint":18.69,"uvIndex":0,"visibility":5623.51,"windDirection":136,"windGust":25.43,"windSpeed":10.70},{"forecastStart":"2022-11-12T00:00:00Z","cloudCover":0.98,"conditionCode":"Drizzle","daylight":false,"humidity":0.89,"precipitationAmount":0.3,"precipitationIntensity":0.3,"precipitationChance":0.37,"precipitationType":"rain","pressure":1006.90,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.46,"temperatureApparent":21.48,"temperatureDewPoint":18.56,"uvIndex":0,"visibility":6763.73,"windDirection":138,"windGust":25.62,"windSpeed":11.45},{"forecastStart":"2022-11-12T01:00:00Z","cloudCover":0.98,"conditionCode":"Drizzle","daylight":false,"humidity":0.90,"precipitationAmount":0.5,"precipitationIntensity":0.5,"precipitationChance":0.35,"precipitationType":"rain","pressure":1006.05,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.42,"temperatureApparent":21.48,"temperatureDewPoint":18.69,"uvIndex":0,"visibility":7419.72,"windDirection":141,"windGust":28.89,"windSpeed":12.77},{"forecastStart":"2022-11-12T02:00:00Z","cloudCover":0.98,"conditionCode":"Drizzle","daylight":false,"humidity":0.90,"precipitationAmount":0.8,"precipitationIntensity":0.8,"precipitationChance":0.34,"precipitationType":"rain","pressure":1005.18,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.32,"temperatureApparent":21.37,"temperatureDewPoint":18.69,"uvIndex":0,"visibility":14000.44,"windDirection":145,"windGust":33.39,"windSpeed":13.85},{"forecastStart":"2022-11-12T03:00:00Z","cloudCover":0.98,"conditionCode":"Rain","daylight":false,"humidity":0.91,"precipitationAmount":1.1,"precipitationIntensity":1.1,"precipitationChance":0.33,"precipitationType":"rain","pressure":1004.31,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.35,"temperatureApparent":21.43,"temperatureDewPoint":18.79,"uvIndex":0,"visibility":12020.86,"windDirection":147,"windGust":34.99,"windSpeed":14.23},{"forecastStart":"2022-11-12T04:00:00Z","cloudCover":0.99,"conditionCode":"Thunderstorms","daylight":false,"humidity":0.91,"precipitationAmount":1.4,"precipitationIntensity":1.4,"precipitationChance":0.33,"precipitationType":"rain","pressure":1003.43,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.39,"temperatureApparent":21.50,"temperatureDewPoint":18.90,"uvIndex":0,"visibility":12764.52,"windDirection":155,"windGust":34.92,"windSpeed":14.69},{"forecastStart":"2022-11-12T05:00:00Z","cloudCover":0.99,"conditionCode":"Thunderstorms","daylight":false,"humidity":0.91,"precipitationAmount":1.6,"precipitationIntensity":1.6,"precipitationChance":0.34,"precipitationType":"rain","pressure":1002.70,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.26,"temperatureApparent":21.35,"temperatureDewPoint":18.83,"uvIndex":0,"visibility":14826.26,"windDirection":175,"windGust":33.13,"windSpeed":14.56},{"forecastStart":"2022-11-12T06:00:00Z","cloudCover":0.99,"conditionCode":"Thunderstorms","daylight":false,"humidity":0.92,"precipitationAmount":1.9,"precipitationIntensity":1.9,"precipitationChance":0.34,"precipitationType":"rain","pressure":1002.05,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.06,"temperatureApparent":21.09,"temperatureDewPoint":18.65,"uvIndex":0,"visibility":15429.09,"windDirection":190,"windGust":30.37,"windSpeed":13.87},{"forecastStart":"2022-11-12T07:00:00Z","cloudCover":0.98,"conditionCode":"Thunderstorms","daylight":false,"humidity":0.92,"precipitationAmount":2.4,"precipitationIntensity":2.4,"precipitationChance":0.35,"precipitationType":"rain","pressure":1001.32,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.86,"temperatureApparent":20.83,"temperatureDewPoint":18.45,"uvIndex":0,"visibility":12863.99,"windDirection":192,"windGust":29.75,"windSpeed":13.01},{"forecastStart":"2022-11-12T08:00:00Z","cloudCover":0.97,"conditionCode":"Thunderstorms","daylight":false,"humidity":0.92,"precipitationAmount":2.8,"precipitationIntensity":2.8,"precipitationChance":0.36,"precipitationType":"rain","pressure":1000.63,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.27,"temperatureApparent":20.09,"temperatureDewPoint":17.89,"uvIndex":0,"visibility":8997.67,"windDirection":186,"windGust":30.16,"windSpeed":12.13},{"forecastStart":"2022-11-12T09:00:00Z","cloudCover":0.95,"conditionCode":"Thunderstorms","daylight":false,"humidity":0.92,"precipitationAmount":3.1,"precipitationIntensity":3.1,"precipitationChance":0.36,"precipitationType":"rain","pressure":1000.33,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":18.97,"temperatureApparent":19.70,"temperatureDewPoint":17.57,"uvIndex":0,"visibility":6425.65,"windDirection":189,"windGust":31.12,"windSpeed":11.94},{"forecastStart":"2022-11-12T10:00:00Z","cloudCover":0.91,"conditionCode":"Rain","daylight":false,"humidity":0.92,"precipitationAmount":3.1,"precipitationIntensity":3.1,"precipitationChance":0.33,"precipitationType":"rain","pressure":1000.64,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":18.50,"temperatureApparent":19.12,"temperatureDewPoint":17.11,"uvIndex":0,"visibility":6321.86,"windDirection":212,"windGust":33.01,"windSpeed":13.17},{"forecastStart":"2022-11-12T11:00:00Z","cloudCover":0.87,"conditionCode":"Rain","daylight":false,"humidity":0.91,"precipitationAmount":2.8,"precipitationIntensity":2.8,"precipitationChance":0.30,"precipitationType":"rain","pressure":1001.34,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":17.96,"temperatureApparent":18.43,"temperatureDewPoint":16.50,"uvIndex":0,"visibility":7506.38,"windDirection":235,"windGust":35.44,"windSpeed":15.11},{"forecastStart":"2022-11-12T12:00:00Z","cloudCover":0.84,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.90,"precipitationAmount":2.4,"precipitationIntensity":2.4,"precipitationChance":0.27,"precipitationType":"rain","pressure":1002.06,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":17.62,"temperatureApparent":17.96,"temperatureDewPoint":15.87,"uvIndex":0,"visibility":9035.43,"windDirection":248,"windGust":36.86,"windSpeed":16.24},{"forecastStart":"2022-11-12T13:00:00Z","cloudCover":0.77,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.86,"precipitationAmount":1.8,"precipitationIntensity":1.8,"precipitationChance":0.23,"precipitationType":"rain","pressure":1002.72,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":17.89,"temperatureApparent":18.17,"temperatureDewPoint":15.54,"uvIndex":0,"visibility":10326.86,"windDirection":253,"windGust":36.46,"windSpeed":15.72},{"forecastStart":"2022-11-12T14:00:00Z","cloudCover":0.68,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.82,"precipitationAmount":1.0,"precipitationIntensity":1.0,"precipitationChance":0.20,"precipitationType":"rain","pressure":1003.41,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":18.52,"temperatureApparent":18.77,"temperatureDewPoint":15.37,"uvIndex":1,"visibility":11962.61,"windDirection":258,"windGust":35.04,"windSpeed":14.38},{"forecastStart":"2022-11-12T15:00:00Z","cloudCover":0.61,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.77,"precipitationAmount":0.4,"precipitationIntensity":0.4,"precipitationChance":0.17,"precipitationType":"rain","pressure":1003.91,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.13,"temperatureApparent":19.30,"temperatureDewPoint":14.94,"uvIndex":2,"visibility":14744.09,"windDirection":264,"windGust":33.34,"windSpeed":13.33},{"forecastStart":"2022-11-12T16:00:00Z","cloudCover":0.48,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.71,"precipitationAmount":0.3,"precipitationIntensity":0.3,"precipitationChance":0.14,"precipitationType":"rain","pressure":1004.05,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.93,"temperatureApparent":20.01,"temperatureDewPoint":14.43,"uvIndex":3,"visibility":19980.16,"windDirection":270,"windGust":31.38,"windSpeed":13.10},{"forecastStart":"2022-11-12T17:00:00Z","cloudCover":0.34,"conditionCode":"MostlyClear","daylight":true,"humidity":0.64,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1004.01,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.95,"temperatureApparent":20.93,"temperatureDewPoint":13.93,"uvIndex":4,"visibility":26356.66,"windDirection":278,"windGust":29.12,"windSpeed":13.15},{"forecastStart":"2022-11-12T18:00:00Z","cloudCover":0.24,"conditionCode":"MostlyClear","daylight":true,"humidity":0.59,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1004.09,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":21.43,"temperatureApparent":21.28,"temperatureDewPoint":13.20,"uvIndex":3,"visibility":30743.84,"windDirection":280,"windGust":27.17,"windSpeed":12.97},{"forecastStart":"2022-11-12T19:00:00Z","cloudCover":0.20,"conditionCode":"MostlyClear","daylight":true,"humidity":0.57,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1004.35,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":21.10,"temperatureApparent":20.79,"temperatureDewPoint":12.22,"uvIndex":2,"visibility":31174.76,"windDirection":280,"windGust":25.36,"windSpeed":12.07},{"forecastStart":"2022-11-12T20:00:00Z","cloudCover":0.20,"conditionCode":"MostlyClear","daylight":true,"humidity":0.55,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1004.72,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":20.39,"temperatureApparent":19.92,"temperatureDewPoint":11.13,"uvIndex":1,"visibility":29621.65,"windDirection":275,"windGust":23.87,"windSpeed":10.93},{"forecastStart":"2022-11-12T21:00:00Z","cloudCover":0.20,"conditionCode":"MostlyClear","daylight":true,"humidity":0.54,"precipitationAmount":0.2,"precipitationIntensity":0.2,"precipitationChance":0.08,"precipitationType":"rain","pressure":1005.33,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":19.27,"temperatureApparent":18.64,"temperatureDewPoint":9.85,"uvIndex":0,"visibility":28859.69,"windDirection":273,"windGust":23.80,"windSpeed":10.45},{"forecastStart":"2022-11-12T22:00:00Z","cloudCover":0.20,"conditionCode":"MostlyClear","daylight":false,"humidity":0.54,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1006.33,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":17.51,"temperatureApparent":16.72,"temperatureDewPoint":8.10,"uvIndex":0,"visibility":30215.29,"windDirection":275,"windGust":26.39,"windSpeed":11.35},{"forecastStart":"2022-11-12T23:00:00Z","cloudCover":0.20,"conditionCode":"MostlyClear","daylight":false,"humidity":0.54,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1007.55,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":15.21,"temperatureApparent":14.31,"temperatureDewPoint":6.04,"uvIndex":0,"visibility":32350.03,"windDirection":280,"windGust":30.37,"windSpeed":12.90},{"forecastStart":"2022-11-13T00:00:00Z","cloudCover":0.19,"conditionCode":"MostlyClear","daylight":false,"humidity":0.55,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1008.62,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":13.33,"temperatureApparent":12.38,"temperatureDewPoint":4.50,"uvIndex":0,"visibility":34045.29,"windDirection":287,"windGust":33.09,"windSpeed":13.83},{"forecastStart":"2022-11-13T01:00:00Z","cloudCover":0.20,"conditionCode":"MostlyClear","daylight":false,"humidity":0.56,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1009.39,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":11.97,"temperatureApparent":11.01,"temperatureDewPoint":3.56,"uvIndex":0,"visibility":34826.22,"windDirection":295,"windGust":33.44,"windSpeed":13.56},{"forecastStart":"2022-11-13T02:00:00Z","cloudCover":0.22,"conditionCode":"MostlyClear","daylight":false,"humidity":0.59,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1010.03,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.82,"temperatureApparent":9.87,"temperatureDewPoint":3.01,"uvIndex":0,"visibility":35206.04,"windDirection":308,"windGust":32.66,"windSpeed":12.71},{"forecastStart":"2022-11-13T03:00:00Z","cloudCover":0.23,"conditionCode":"MostlyClear","daylight":false,"humidity":0.60,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1010.57,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.75,"temperatureApparent":8.04,"temperatureDewPoint":2.42,"uvIndex":0,"visibility":35452.29,"windDirection":318,"windGust":31.59,"windSpeed":11.81},{"forecastStart":"2022-11-13T04:00:00Z","cloudCover":0.25,"conditionCode":"MostlyClear","daylight":false,"humidity":0.62,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1011.02,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.70,"temperatureApparent":6.87,"temperatureDewPoint":1.75,"uvIndex":0,"visibility":35619.42,"windDirection":327,"windGust":30.68,"windSpeed":11.17},{"forecastStart":"2022-11-13T05:00:00Z","cloudCover":0.27,"conditionCode":"MostlyClear","daylight":false,"humidity":0.63,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1011.40,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":7.74,"temperatureApparent":5.82,"temperatureDewPoint":1.05,"uvIndex":0,"visibility":35666.25,"windDirection":334,"windGust":29.74,"windSpeed":10.61},{"forecastStart":"2022-11-13T06:00:00Z","cloudCover":0.31,"conditionCode":"MostlyClear","daylight":false,"humidity":0.64,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1011.80,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":6.95,"temperatureApparent":5.00,"temperatureDewPoint":0.56,"uvIndex":0,"visibility":35710.78,"windDirection":338,"windGust":28.68,"windSpeed":9.95},{"forecastStart":"2022-11-13T07:00:00Z","cloudCover":0.37,"conditionCode":"MostlyClear","daylight":false,"humidity":0.65,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1012.25,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":6.37,"temperatureApparent":4.51,"temperatureDewPoint":0.33,"uvIndex":0,"visibility":35763.32,"windDirection":339,"windGust":27.33,"windSpeed":8.99},{"forecastStart":"2022-11-13T08:00:00Z","cloudCover":0.46,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.67,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1012.73,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.81,"temperatureApparent":4.12,"temperatureDewPoint":0.15,"uvIndex":0,"visibility":35765.38,"windDirection":337,"windGust":25.67,"windSpeed":7.85},{"forecastStart":"2022-11-13T09:00:00Z","cloudCover":0.55,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.69,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1013.21,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.41,"temperatureApparent":3.94,"temperatureDewPoint":0.09,"uvIndex":0,"visibility":35734.11,"windDirection":336,"windGust":23.88,"windSpeed":6.81},{"forecastStart":"2022-11-13T10:00:00Z","cloudCover":0.61,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.70,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1013.68,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":4.94,"temperatureApparent":3.67,"temperatureDewPoint":-0.03,"uvIndex":0,"visibility":35700.42,"windDirection":336,"windGust":21.94,"windSpeed":5.96},{"forecastStart":"2022-11-13T11:00:00Z","cloudCover":0.65,"conditionCode":"MostlyCloudy","daylight":false,"humidity":0.72,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1014.12,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":4.53,"temperatureApparent":3.45,"temperatureDewPoint":-0.17,"uvIndex":0,"visibility":35716.45,"windDirection":337,"windGust":20.03,"windSpeed":5.25},{"forecastStart":"2022-11-13T12:00:00Z","cloudCover":0.68,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.71,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1014.50,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":4.41,"temperatureApparent":3.48,"temperatureDewPoint":-0.35,"uvIndex":0,"visibility":35832.40,"windDirection":334,"windGust":18.62,"windSpeed":4.81},{"forecastStart":"2022-11-13T13:00:00Z","cloudCover":0.73,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.69,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1014.84,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.07,"temperatureApparent":4.18,"temperatureDewPoint":-0.25,"uvIndex":0,"visibility":36125.65,"windDirection":323,"windGust":17.92,"windSpeed":4.78},{"forecastStart":"2022-11-13T14:00:00Z","cloudCover":0.77,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.64,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1015.09,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":6.09,"temperatureApparent":5.17,"temperatureDewPoint":-0.16,"uvIndex":1,"visibility":36576.27,"windDirection":303,"windGust":17.78,"windSpeed":5.05},{"forecastStart":"2022-11-13T15:00:00Z","cloudCover":0.76,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.60,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1015.10,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":7.30,"temperatureApparent":6.35,"temperatureDewPoint":0.05,"uvIndex":2,"visibility":37085.80,"windDirection":286,"windGust":18.08,"windSpeed":5.43},{"forecastStart":"2022-11-13T16:00:00Z","cloudCover":0.65,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.56,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1014.75,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.33,"temperatureApparent":7.34,"temperatureDewPoint":0.15,"uvIndex":3,"visibility":37555.66,"windDirection":278,"windGust":18.73,"windSpeed":5.82},{"forecastStart":"2022-11-13T17:00:00Z","cloudCover":0.51,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.53,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1014.22,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.36,"temperatureApparent":8.34,"temperatureDewPoint":0.34,"uvIndex":3,"visibility":37886.55,"windDirection":275,"windGust":19.53,"windSpeed":6.22},{"forecastStart":"2022-11-13T18:00:00Z","cloudCover":0.41,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.52,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1013.87,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.98,"temperatureApparent":8.94,"temperatureDewPoint":0.46,"uvIndex":3,"visibility":37979.76,"windDirection":272,"windGust":20.21,"windSpeed":6.60},{"forecastStart":"2022-11-13T19:00:00Z","cloudCover":0.39,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.51,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1013.76,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.26,"temperatureApparent":9.22,"temperatureDewPoint":0.56,"uvIndex":2,"visibility":37786.07,"windDirection":267,"windGust":20.72,"windSpeed":7.07},{"forecastStart":"2022-11-13T20:00:00Z","cloudCover":0.39,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.51,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1013.79,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.03,"temperatureApparent":8.99,"temperatureDewPoint":0.45,"uvIndex":1,"visibility":37373.39,"windDirection":261,"windGust":21.19,"windSpeed":7.57},{"forecastStart":"2022-11-13T21:00:00Z","cloudCover":0.41,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.53,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1014.04,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.54,"temperatureApparent":8.50,"temperatureDewPoint":0.48,"uvIndex":0,"visibility":36819.96,"windDirection":258,"windGust":21.56,"windSpeed":7.84},{"forecastStart":"2022-11-13T22:00:00Z","cloudCover":0.41,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.56,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1014.57,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.71,"temperatureApparent":7.56,"temperatureDewPoint":0.52,"uvIndex":0,"visibility":36203.89,"windDirection":264,"windGust":21.97,"windSpeed":7.70},{"forecastStart":"2022-11-13T23:00:00Z","cloudCover":0.41,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.61,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1015.29,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":7.71,"temperatureApparent":6.47,"temperatureDewPoint":0.70,"uvIndex":0,"visibility":35603.79,"windDirection":273,"windGust":22.40,"windSpeed":7.35},{"forecastStart":"2022-11-14T00:00:00Z","cloudCover":0.40,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.66,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1015.95,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":6.96,"temperatureApparent":5.65,"temperatureDewPoint":0.98,"uvIndex":0,"visibility":35097.83,"windDirection":280,"windGust":22.65,"windSpeed":7.14},{"forecastStart":"2022-11-14T01:00:00Z","cloudCover":0.21,"conditionCode":"MostlyClear","daylight":false,"humidity":0.67,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1016.50,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.59,"temperatureApparent":4.03,"temperatureDewPoint":0.01,"uvIndex":0,"visibility":42681.58,"windDirection":287,"windGust":23.50,"windSpeed":7.26},{"forecastStart":"2022-11-14T02:00:00Z","cloudCover":0.19,"conditionCode":"MostlyClear","daylight":false,"humidity":0.70,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1017.09,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":4.77,"temperatureApparent":3.01,"temperatureDewPoint":-0.20,"uvIndex":0,"visibility":42306.66,"windDirection":292,"windGust":22.16,"windSpeed":7.47},{"forecastStart":"2022-11-14T03:00:00Z","cloudCover":0.17,"conditionCode":"MostlyClear","daylight":false,"humidity":0.73,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1017.63,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":3.96,"temperatureApparent":2.06,"temperatureDewPoint":-0.44,"uvIndex":0,"visibility":41957.21,"windDirection":297,"windGust":20.73,"windSpeed":7.47},{"forecastStart":"2022-11-14T04:00:00Z","cloudCover":0.11,"conditionCode":"Clear","daylight":false,"humidity":0.75,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1018.08,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":3.11,"temperatureApparent":1.19,"temperatureDewPoint":-0.87,"uvIndex":0,"visibility":41610.86,"windDirection":303,"windGust":19.46,"windSpeed":7.08},{"forecastStart":"2022-11-14T05:00:00Z","cloudCover":0.05,"conditionCode":"Clear","daylight":false,"humidity":0.77,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1018.46,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":2.30,"temperatureApparent":0.42,"temperatureDewPoint":-1.37,"uvIndex":0,"visibility":41244.65,"windDirection":308,"windGust":18.29,"windSpeed":6.56},{"forecastStart":"2022-11-14T06:00:00Z","cloudCover":0.00,"conditionCode":"Clear","daylight":false,"humidity":0.79,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1018.84,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":1.63,"temperatureApparent":-0.22,"temperatureDewPoint":-1.69,"uvIndex":0,"visibility":40834.36,"windDirection":312,"windGust":17.06,"windSpeed":6.19},{"forecastStart":"2022-11-14T07:00:00Z","cloudCover":0.01,"conditionCode":"Clear","daylight":false,"humidity":0.82,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1019.23,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":0.91,"temperatureApparent":-1.01,"temperatureDewPoint":-1.90,"uvIndex":0,"visibility":40233.32,"windDirection":315,"windGust":15.44,"windSpeed":6.08},{"forecastStart":"2022-11-14T08:00:00Z","cloudCover":0.02,"conditionCode":"Clear","daylight":false,"humidity":0.85,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1019.64,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":0.44,"temperatureApparent":-1.56,"temperatureDewPoint":-1.83,"uvIndex":0,"visibility":39428.95,"windDirection":318,"windGust":13.67,"windSpeed":6.11},{"forecastStart":"2022-11-14T09:00:00Z","cloudCover":0.02,"conditionCode":"Clear","daylight":false,"humidity":0.87,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1020.12,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":0.03,"temperatureApparent":-2.05,"temperatureDewPoint":-1.86,"uvIndex":0,"visibility":38602.19,"windDirection":318,"windGust":12.45,"windSpeed":6.15},{"forecastStart":"2022-11-14T10:00:00Z","cloudCover":0.03,"conditionCode":"Clear","daylight":false,"humidity":0.87,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1020.69,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":-0.40,"temperatureApparent":-2.56,"temperatureDewPoint":-2.23,"uvIndex":0,"visibility":37936.52,"windDirection":316,"windGust":12.31,"windSpeed":6.17},{"forecastStart":"2022-11-14T11:00:00Z","cloudCover":0.04,"conditionCode":"Clear","daylight":false,"humidity":0.86,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1021.30,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":-0.76,"temperatureApparent":-2.98,"temperatureDewPoint":-2.80,"uvIndex":0,"visibility":37614.84,"windDirection":315,"windGust":12.76,"windSpeed":6.20},{"forecastStart":"2022-11-14T12:00:00Z","cloudCover":0.05,"conditionCode":"Clear","daylight":true,"humidity":0.83,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1021.89,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":0.02,"temperatureApparent":-2.08,"temperatureDewPoint":-2.57,"uvIndex":0,"visibility":37823.92,"windDirection":314,"windGust":13.01,"windSpeed":6.18},{"forecastStart":"2022-11-14T13:00:00Z","cloudCover":0.14,"conditionCode":"MostlyClear","daylight":true,"humidity":0.82,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1024.76,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":0.67,"temperatureApparent":-1.08,"temperatureDewPoint":-1.99,"uvIndex":0,"visibility":38830.52,"windDirection":278,"windGust":12.76,"windSpeed":5.57},{"forecastStart":"2022-11-14T14:00:00Z","cloudCover":0.22,"conditionCode":"MostlyClear","daylight":true,"humidity":0.73,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1025.17,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":2.82,"temperatureApparent":1.40,"temperatureDewPoint":-1.63,"uvIndex":1,"visibility":40545.21,"windDirection":274,"windGust":12.66,"windSpeed":5.52},{"forecastStart":"2022-11-14T15:00:00Z","cloudCover":0.25,"conditionCode":"MostlyClear","daylight":true,"humidity":0.64,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1025.32,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.06,"temperatureApparent":3.92,"temperatureDewPoint":-1.27,"uvIndex":2,"visibility":42616.11,"windDirection":270,"windGust":13.34,"windSpeed":5.65},{"forecastStart":"2022-11-14T16:00:00Z","cloudCover":0.20,"conditionCode":"MostlyClear","daylight":true,"humidity":0.56,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1025.07,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":7.05,"temperatureApparent":6.06,"temperatureDewPoint":-1.14,"uvIndex":3,"visibility":44688.32,"windDirection":265,"windGust":14.93,"windSpeed":6.04},{"forecastStart":"2022-11-14T17:00:00Z","cloudCover":0.11,"conditionCode":"Clear","daylight":true,"humidity":0.50,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1024.66,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.53,"temperatureApparent":7.48,"temperatureDewPoint":-1.29,"uvIndex":3,"visibility":46404.23,"windDirection":260,"windGust":16.59,"windSpeed":6.49},{"forecastStart":"2022-11-14T18:00:00Z","cloudCover":0.03,"conditionCode":"Clear","daylight":true,"humidity":0.47,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1024.43,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.50,"temperatureApparent":8.41,"temperatureDewPoint":-1.32,"uvIndex":3,"visibility":47403.97,"windDirection":256,"windGust":17.49,"windSpeed":6.69},{"forecastStart":"2022-11-14T19:00:00Z","cloudCover":0.03,"conditionCode":"Clear","daylight":true,"humidity":0.46,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1024.50,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.89,"temperatureApparent":8.79,"temperatureDewPoint":-1.11,"uvIndex":2,"visibility":47562.12,"windDirection":254,"windGust":17.31,"windSpeed":6.44},{"forecastStart":"2022-11-14T20:00:00Z","cloudCover":0.02,"conditionCode":"Clear","daylight":true,"humidity":0.48,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1024.73,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.55,"temperatureApparent":8.47,"temperatureDewPoint":-1.04,"uvIndex":1,"visibility":47137.25,"windDirection":252,"windGust":16.37,"windSpeed":5.95},{"forecastStart":"2022-11-14T21:00:00Z","cloudCover":0.02,"conditionCode":"Clear","daylight":true,"humidity":0.50,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1025.15,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.77,"temperatureApparent":7.72,"temperatureDewPoint":-0.98,"uvIndex":0,"visibility":46343.57,"windDirection":249,"windGust":14.84,"windSpeed":5.54},{"forecastStart":"2022-11-14T22:00:00Z","cloudCover":0.04,"conditionCode":"Clear","daylight":false,"humidity":0.55,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1025.82,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":7.46,"temperatureApparent":6.46,"temperatureDewPoint":-1.02,"uvIndex":0,"visibility":45398.34,"windDirection":243,"windGust":12.90,"windSpeed":5.41},{"forecastStart":"2022-11-14T23:00:00Z","cloudCover":0.07,"conditionCode":"Clear","daylight":false,"humidity":0.61,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1026.63,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.88,"temperatureApparent":4.92,"temperatureDewPoint":-1.14,"uvIndex":0,"visibility":44519.39,"windDirection":232,"windGust":11.09,"windSpeed":5.45},{"forecastStart":"2022-11-15T00:00:00Z","cloudCover":0.12,"conditionCode":"Clear","daylight":false,"humidity":0.66,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.36,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":4.46,"temperatureApparent":3.28,"temperatureDewPoint":-1.35,"uvIndex":0,"visibility":43926.81,"windDirection":224,"windGust":10.11,"windSpeed":5.52},{"forecastStart":"2022-11-15T01:00:00Z","cloudCover":0.18,"conditionCode":"MostlyClear","daylight":false,"humidity":0.69,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.96,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":3.63,"temperatureApparent":2.30,"temperatureDewPoint":-1.49,"uvIndex":0,"visibility":43627.20,"windDirection":216,"windGust":10.36,"windSpeed":5.57},{"forecastStart":"2022-11-15T02:00:00Z","cloudCover":0.25,"conditionCode":"MostlyClear","daylight":false,"humidity":0.72,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1028.52,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":2.96,"temperatureApparent":1.51,"temperatureDewPoint":-1.58,"uvIndex":0,"visibility":43446.88,"windDirection":210,"windGust":11.27,"windSpeed":5.64},{"forecastStart":"2022-11-15T03:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":false,"humidity":0.75,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1029.05,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":2.32,"temperatureApparent":0.75,"temperatureDewPoint":-1.70,"uvIndex":0,"visibility":43336.70,"windDirection":189,"windGust":12.04,"windSpeed":5.73},{"forecastStart":"2022-11-15T04:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":false,"humidity":0.77,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1029.57,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":1.71,"temperatureApparent":0.02,"temperatureDewPoint":-1.87,"uvIndex":0,"visibility":43245.93,"windDirection":28,"windGust":12.12,"windSpeed":5.81},{"forecastStart":"2022-11-15T05:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":false,"humidity":0.79,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1030.04,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":1.18,"temperatureApparent":-0.61,"temperatureDewPoint":-2.04,"uvIndex":0,"visibility":43124.00,"windDirection":20,"windGust":11.69,"windSpeed":5.83},{"forecastStart":"2022-11-15T06:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":false,"humidity":0.81,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1030.49,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":0.72,"temperatureApparent":-1.11,"temperatureDewPoint":-2.18,"uvIndex":0,"visibility":42920.06,"windDirection":18,"windGust":11.07,"windSpeed":5.79},{"forecastStart":"2022-11-15T07:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":false,"humidity":0.83,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1030.92,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":0.15,"temperatureApparent":-1.70,"temperatureDewPoint":-2.41,"uvIndex":0,"visibility":42515.48,"windDirection":17,"windGust":10.29,"windSpeed":5.63},{"forecastStart":"2022-11-15T08:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":false,"humidity":0.85,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.35,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":-0.33,"temperatureApparent":-2.16,"temperatureDewPoint":-2.54,"uvIndex":0,"visibility":41926.02,"windDirection":15,"windGust":9.36,"windSpeed":5.43},{"forecastStart":"2022-11-15T09:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":false,"humidity":0.87,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.81,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":-0.73,"temperatureApparent":-2.56,"temperatureDewPoint":-2.64,"uvIndex":0,"visibility":41303.76,"windDirection":15,"windGust":8.53,"windSpeed":5.28},{"forecastStart":"2022-11-15T10:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":false,"humidity":0.89,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1032.31,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":-1.12,"temperatureApparent":-3.00,"temperatureDewPoint":-2.74,"uvIndex":0,"visibility":40800.92,"windDirection":15,"windGust":8.12,"windSpeed":5.31},{"forecastStart":"2022-11-15T11:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":false,"humidity":0.90,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1032.79,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":-1.33,"temperatureApparent":-3.29,"temperatureDewPoint":-2.83,"uvIndex":0,"visibility":40572.04,"windDirection":18,"windGust":8.10,"windSpeed":5.41},{"forecastStart":"2022-11-15T12:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":true,"humidity":0.88,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.16,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":-0.74,"temperatureApparent":-2.63,"temperatureDewPoint":-2.55,"uvIndex":0,"visibility":40770.96,"windDirection":22,"windGust":8.28,"windSpeed":5.42},{"forecastStart":"2022-11-15T13:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":true,"humidity":0.81,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.45,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":0.84,"temperatureApparent":-0.71,"temperatureDewPoint":-2.05,"uvIndex":0,"visibility":41636.75,"windDirection":28,"windGust":8.67,"windSpeed":5.17},{"forecastStart":"2022-11-15T14:00:00Z","cloudCover":0.30,"conditionCode":"MostlyClear","daylight":true,"humidity":0.72,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.64,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":3.12,"temperatureApparent":2.26,"temperatureDewPoint":-1.38,"uvIndex":1,"visibility":43095.86,"windDirection":38,"windGust":9.23,"windSpeed":4.74},{"forecastStart":"2022-11-15T15:00:00Z","cloudCover":0.29,"conditionCode":"MostlyClear","daylight":true,"humidity":0.64,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.56,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.53,"temperatureApparent":4.62,"temperatureDewPoint":-0.66,"uvIndex":2,"visibility":44838.15,"windDirection":51,"windGust":9.70,"windSpeed":4.27},{"forecastStart":"2022-11-15T16:00:00Z","cloudCover":0.29,"conditionCode":"MostlyClear","daylight":true,"humidity":0.58,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1033.13,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":7.65,"temperatureApparent":6.67,"temperatureDewPoint":-0.08,"uvIndex":3,"visibility":46549.11,"windDirection":69,"windGust":9.83,"windSpeed":3.88},{"forecastStart":"2022-11-15T17:00:00Z","cloudCover":0.29,"conditionCode":"MostlyClear","daylight":true,"humidity":0.53,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1032.51,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.34,"temperatureApparent":8.32,"temperatureDewPoint":0.34,"uvIndex":3,"visibility":47913.72,"windDirection":91,"windGust":9.66,"windSpeed":3.59},{"forecastStart":"2022-11-15T18:00:00Z","cloudCover":0.28,"conditionCode":"MostlyClear","daylight":true,"humidity":0.51,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.98,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.44,"temperatureApparent":9.39,"temperatureDewPoint":0.75,"uvIndex":3,"visibility":48613.78,"windDirection":104,"windGust":9.43,"windSpeed":3.49},{"forecastStart":"2022-11-15T19:00:00Z","cloudCover":0.28,"conditionCode":"MostlyClear","daylight":true,"humidity":0.51,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.60,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.82,"temperatureApparent":9.78,"temperatureDewPoint":1.19,"uvIndex":2,"visibility":48556.40,"windDirection":106,"windGust":9.08,"windSpeed":3.62},{"forecastStart":"2022-11-15T20:00:00Z","cloudCover":0.27,"conditionCode":"MostlyClear","daylight":true,"humidity":0.54,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.29,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.42,"temperatureApparent":9.40,"temperatureDewPoint":1.38,"uvIndex":1,"visibility":47973.28,"windDirection":100,"windGust":8.73,"windSpeed":3.95},{"forecastStart":"2022-11-15T21:00:00Z","cloudCover":0.24,"conditionCode":"MostlyClear","daylight":true,"humidity":0.58,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.13,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.54,"temperatureApparent":8.56,"temperatureDewPoint":1.56,"uvIndex":0,"visibility":47033.15,"windDirection":98,"windGust":8.83,"windSpeed":4.45},{"forecastStart":"2022-11-15T22:00:00Z","cloudCover":0.17,"conditionCode":"MostlyClear","daylight":false,"humidity":0.63,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.17,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.14,"temperatureApparent":7.21,"temperatureDewPoint":1.55,"uvIndex":0,"visibility":45904.79,"windDirection":99,"windGust":10.04,"windSpeed":5.04},{"forecastStart":"2022-11-15T23:00:00Z","cloudCover":0.08,"conditionCode":"Clear","daylight":false,"humidity":0.71,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.34,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":6.50,"temperatureApparent":5.59,"temperatureDewPoint":1.53,"uvIndex":0,"visibility":44759.64,"windDirection":102,"windGust":11.80,"windSpeed":5.58},{"forecastStart":"2022-11-16T00:00:00Z","cloudCover":0.02,"conditionCode":"Clear","daylight":false,"humidity":0.77,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.42,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":5.11,"temperatureApparent":3.87,"temperatureDewPoint":1.34,"uvIndex":0,"visibility":43769.72,"windDirection":104,"windGust":12.80,"windSpeed":5.96},{"forecastStart":"2022-11-16T01:00:00Z","cloudCover":0.08,"conditionCode":"Clear","daylight":false,"humidity":0.80,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.38,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":4.37,"temperatureApparent":2.95,"temperatureDewPoint":1.27,"uvIndex":0,"visibility":43027.88,"windDirection":105,"windGust":12.29,"windSpeed":6.14},{"forecastStart":"2022-11-16T02:00:00Z","cloudCover":0.18,"conditionCode":"MostlyClear","daylight":false,"humidity":0.83,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.26,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":3.82,"temperatureApparent":2.28,"temperatureDewPoint":1.18,"uvIndex":0,"visibility":42416.18,"windDirection":107,"windGust":11.05,"windSpeed":6.25},{"forecastStart":"2022-11-16T03:00:00Z","cloudCover":0.27,"conditionCode":"MostlyClear","daylight":false,"humidity":0.85,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1031.04,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":3.33,"temperatureApparent":1.68,"temperatureDewPoint":1.07,"uvIndex":0,"visibility":41789.49,"windDirection":112,"windGust":10.09,"windSpeed":6.34},{"forecastStart":"2022-11-16T04:00:00Z","cloudCover":0.34,"conditionCode":"MostlyClear","daylight":false,"humidity":0.87,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1030.64,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":2.84,"temperatureApparent":1.06,"temperatureDewPoint":0.95,"uvIndex":0,"visibility":41002.06,"windDirection":121,"windGust":9.79,"windSpeed":6.51},{"forecastStart":"2022-11-16T05:00:00Z","cloudCover":0.38,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.90,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1030.14,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":2.25,"temperatureApparent":0.32,"temperatureDewPoint":0.71,"uvIndex":0,"visibility":39905.77,"windDirection":134,"windGust":9.64,"windSpeed":6.68},{"forecastStart":"2022-11-16T06:00:00Z","cloudCover":0.42,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.91,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1029.70,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":1.88,"temperatureApparent":-0.11,"temperatureDewPoint":0.61,"uvIndex":0,"visibility":38354.32,"windDirection":147,"windGust":9.30,"windSpeed":6.68},{"forecastStart":"2022-11-16T07:00:00Z","cloudCover":0.42,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.93,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1029.38,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":1.55,"temperatureApparent":-0.34,"temperatureDewPoint":0.53,"uvIndex":0,"visibility":35731.50,"windDirection":160,"windGust":8.34,"windSpeed":6.27},{"forecastStart":"2022-11-16T08:00:00Z","cloudCover":0.44,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.94,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1029.12,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":1.26,"temperatureApparent":-0.40,"temperatureDewPoint":0.45,"uvIndex":0,"visibility":32027.24,"windDirection":200,"windGust":6.91,"windSpeed":5.57},{"forecastStart":"2022-11-16T09:00:00Z","cloudCover":0.46,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.95,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1028.90,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":1.09,"temperatureApparent":-0.30,"temperatureDewPoint":0.40,"uvIndex":0,"visibility":28003.82,"windDirection":314,"windGust":5.58,"windSpeed":4.89},{"forecastStart":"2022-11-16T10:00:00Z","cloudCover":0.59,"conditionCode":"PartlyCloudy","daylight":false,"humidity":0.95,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1028.70,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":1.30,"temperatureApparent":0.56,"temperatureDewPoint":0.61,"uvIndex":0,"visibility":24428.13,"windDirection":334,"windGust":4.84,"windSpeed":4.52},{"forecastStart":"2022-11-16T11:00:00Z","cloudCover":0.77,"conditionCode":"MostlyCloudy","daylight":false,"humidity":0.94,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1028.53,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":1.69,"temperatureApparent":0.94,"temperatureDewPoint":0.81,"uvIndex":0,"visibility":22071.86,"windDirection":339,"windGust":4.87,"windSpeed":4.43},{"forecastStart":"2022-11-16T12:00:00Z","cloudCover":0.88,"conditionCode":"Cloudy","daylight":true,"humidity":0.91,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1028.32,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":2.49,"temperatureApparent":1.73,"temperatureDewPoint":1.13,"uvIndex":0,"visibility":21712.10,"windDirection":336,"windGust":5.76,"windSpeed":4.50},{"forecastStart":"2022-11-16T13:00:00Z","cloudCover":0.86,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.84,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1028.05,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":4.07,"temperatureApparent":3.28,"temperatureDewPoint":1.62,"uvIndex":0,"visibility":24130.84,"windDirection":320,"windGust":7.90,"windSpeed":4.68},{"forecastStart":"2022-11-16T14:00:00Z","cloudCover":0.79,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.76,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.73,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":6.32,"temperatureApparent":5.49,"temperatureDewPoint":2.36,"uvIndex":1,"visibility":28877.47,"windDirection":287,"windGust":11.27,"windSpeed":5.10},{"forecastStart":"2022-11-16T15:00:00Z","cloudCover":0.71,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.69,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.29,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.41,"temperatureApparent":7.54,"temperatureDewPoint":2.99,"uvIndex":2,"visibility":34878.25,"windDirection":262,"windGust":15.27,"windSpeed":5.95},{"forecastStart":"2022-11-16T16:00:00Z","cloudCover":0.64,"conditionCode":"MostlyCloudy","daylight":true,"humidity":0.64,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1026.68,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.99,"temperatureApparent":9.09,"temperatureDewPoint":3.41,"uvIndex":3,"visibility":41051.32,"windDirection":255,"windGust":19.29,"windSpeed":7.21},{"forecastStart":"2022-11-16T17:00:00Z","cloudCover":0.58,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.60,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1026.03,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":11.17,"temperatureApparent":10.25,"temperatureDewPoint":3.79,"uvIndex":3,"visibility":46310.02,"windDirection":254,"windGust":22.54,"windSpeed":8.50},{"forecastStart":"2022-11-16T18:00:00Z","cloudCover":0.56,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.59,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1025.53,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":11.86,"temperatureApparent":10.93,"temperatureDewPoint":4.07,"uvIndex":3,"visibility":49559.23,"windDirection":255,"windGust":24.10,"windSpeed":9.35},{"forecastStart":"2022-11-16T19:00:00Z","cloudCover":0.58,"conditionCode":"PartlyCloudy","daylight":true,"humidity":0.59,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1025.30,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":11.98,"temperatureApparent":11.07,"temperatureDewPoint":4.31,"uvIndex":2,"visibility":50580.92,"windDirection":260,"windGust":23.54,"windSpeed":9.67},{"forecastStart":"2022-11-16T20:00:00Z","cloudCover":0.63,"conditionCode":"Drizzle","daylight":true,"humidity":0.61,"precipitationAmount":0.3,"precipitationIntensity":0.3,"precipitationChance":0.11,"precipitationType":"rain","pressure":1025.23,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":11.46,"temperatureApparent":10.56,"temperatureDewPoint":4.25,"uvIndex":1,"visibility":50198.28,"windDirection":265,"windGust":21.57,"windSpeed":9.66},{"forecastStart":"2022-11-16T21:00:00Z","cloudCover":0.70,"conditionCode":"Drizzle","daylight":true,"humidity":0.64,"precipitationAmount":0.4,"precipitationIntensity":0.4,"precipitationChance":0.11,"precipitationType":"rain","pressure":1025.23,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":10.74,"temperatureApparent":9.86,"temperatureDewPoint":4.20,"uvIndex":0,"visibility":48874.83,"windDirection":269,"windGust":19.04,"windSpeed":9.28},{"forecastStart":"2022-11-16T22:00:00Z","cloudCover":0.80,"conditionCode":"Drizzle","daylight":false,"humidity":0.67,"precipitationAmount":0.5,"precipitationIntensity":0.5,"precipitationChance":0.10,"precipitationType":"rain","pressure":1025.28,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.93,"temperatureApparent":8.81,"temperatureDewPoint":4.15,"uvIndex":0,"visibility":47078.95,"windDirection":265,"windGust":16.59,"windSpeed":8.57},{"forecastStart":"2022-11-16T23:00:00Z","cloudCover":0.89,"conditionCode":"Cloudy","daylight":false,"humidity":0.71,"precipitationAmount":0.6,"precipitationIntensity":0.6,"precipitationChance":0.10,"precipitationType":"rain","pressure":1025.36,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":9.08,"temperatureApparent":7.99,"temperatureDewPoint":4.08,"uvIndex":0,"visibility":45277.42,"windDirection":254,"windGust":14.39,"windSpeed":7.70},{"forecastStart":"2022-11-17T00:00:00Z","cloudCover":0.98,"conditionCode":"Cloudy","daylight":false,"humidity":0.74,"precipitationAmount":0.6,"precipitationIntensity":0.6,"precipitationChance":0.09,"precipitationType":"rain","pressure":1025.40,"pressureTrend":"steady","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.25,"temperatureApparent":7.22,"temperatureDewPoint":3.95,"uvIndex":0,"visibility":43934.65,"windDirection":242,"windGust":12.54,"windSpeed":6.88},{"forecastStart":"2022-11-17T01:00:00Z","cloudCover":1.00,"conditionCode":"Cloudy","daylight":false,"humidity":0.79,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.71,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.60,"temperatureApparent":7.85,"temperatureDewPoint":5.26,"uvIndex":0,"visibility":24482.65,"windDirection":177,"windGust":5.84,"windSpeed":3.54},{"forecastStart":"2022-11-17T02:00:00Z","cloudCover":1.00,"conditionCode":"Cloudy","daylight":false,"humidity":0.81,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1027.37,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.41,"temperatureApparent":7.66,"temperatureDewPoint":5.26,"uvIndex":0,"visibility":24811.60,"windDirection":191,"windGust":5.98,"windSpeed":3.35},{"forecastStart":"2022-11-17T03:00:00Z","cloudCover":1.00,"conditionCode":"Cloudy","daylight":false,"humidity":0.81,"precipitationAmount":0.0,"precipitationIntensity":0.0,"precipitationChance":0.00,"precipitationType":"clear","pressure":1026.90,"pressureTrend":"rising","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":8.13,"temperatureApparent":7.38,"temperatureDewPoint":5.14,"uvIndex":0,"visibility":23965.23,"windDirection":204,"windGust":6.61,"windSpeed":3.31},{"forecastStart":"2022-11-17T04:00:00Z","cloudCover":1.00,"conditionCode":"Cloudy","daylight":false,"humidity":0.82,"precipitationAmount":0.2,"precipitationIntensity":0.2,"precipitationChance":0.08,"precipitationType":"rain","pressure":1026.24,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":7.95,"temperatureApparent":7.20,"temperatureDewPoint":5.07,"uvIndex":0,"visibility":20392.56,"windDirection":205,"windGust":8.03,"windSpeed":3.59},{"forecastStart":"2022-11-17T05:00:00Z","cloudCover":1.00,"conditionCode":"Cloudy","daylight":false,"humidity":0.82,"precipitationAmount":0.6,"precipitationIntensity":0.6,"precipitationChance":0.09,"precipitationType":"rain","pressure":1025.46,"pressureTrend":"falling","snowfallIntensity":0.00,"snowfallAmount":0.00,"temperature":7.79,"temperatureApparent":7.04,"temperatureDewPoint":4.98,"uvIndex":0,"visibility":15644.32,"windDirection":203,"windGust":9.95,"windSpeed":4.02}]} + {"metadata":{"latitude":40.709999,"longitude":-74.010002,"attributionUrl":"https://developer.apple.com/weatherkit/data-source-attribution/","readTime":1760993102,"expireTime":1760996421,"temporarilyUnavailable":false,"sourceType":"MODELED","reportedTime":1760961934},"hours":[{"daylight":true,"humidity":60,"pressure":1009.330017,"temperature":16.59,"visibility":34214.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":66,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":8.792908,"windGust":49.967999,"temperatureApparent":11.496218,"cloudCover":74,"precipitationChance":0,"cloudCoverMidAltPct":50,"conditionCode":"WINDY","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1760990400,"pressureTrend":"RISING","windDirection":276,"uvIndex":1,"windSpeed":25.819199,"snowfallIntensity":0.0},{"daylight":true,"humidity":59,"pressure":1010.030029,"temperature":16.4,"visibility":34652.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":29,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":8.365891,"windGust":48.132,"temperatureApparent":11.785448,"cloudCover":61,"precipitationChance":0,"cloudCoverMidAltPct":43,"conditionCode":"PARTLY_CLOUDY","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1760994000,"pressureTrend":"RISING","windDirection":276,"uvIndex":0,"windSpeed":24.0336,"snowfallIntensity":0.0},{"daylight":true,"humidity":58,"pressure":1010.640015,"temperature":15.91,"visibility":34401.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":24,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":7.654709,"windGust":42.588001,"temperatureApparent":10.978209,"cloudCover":34,"precipitationChance":0,"cloudCoverMidAltPct":14,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1760997600,"pressureTrend":"RISING","windDirection":276,"uvIndex":0,"windSpeed":20.6964,"snowfallIntensity":0.0},{"daylight":false,"humidity":60,"pressure":1011.280029,"temperature":15.22,"visibility":34123.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":26,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":7.503769,"windGust":38.051998,"temperatureApparent":10.461295,"cloudCover":27,"precipitationChance":0,"cloudCoverMidAltPct":1,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761001200,"pressureTrend":"RISING","windDirection":269,"uvIndex":0,"windSpeed":18.7848,"snowfallIntensity":0.0},{"daylight":false,"humidity":62,"pressure":1012.01001,"temperature":14.52,"visibility":33566.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":13,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":7.323807,"windGust":36.792,"temperatureApparent":9.444345,"cloudCover":16,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761004800,"pressureTrend":"RISING","windDirection":264,"uvIndex":0,"windSpeed":18.9828,"snowfallIntensity":0.0},{"daylight":false,"humidity":63,"pressure":1012.51001,"temperature":13.86,"visibility":33488.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":4,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.933304,"windGust":31.406401,"temperatureApparent":8.909959,"cloudCover":8,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761008400,"pressureTrend":"RISING","windDirection":264,"uvIndex":0,"windSpeed":17.4312,"snowfallIntensity":0.0},{"daylight":false,"humidity":64,"pressure":1012.969971,"temperature":13.26,"visibility":33081.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":1,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.594101,"windGust":29.599201,"temperatureApparent":8.454525,"cloudCover":3,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761012000,"pressureTrend":"RISING","windDirection":264,"uvIndex":0,"windSpeed":16.232399,"snowfallIntensity":0.0},{"daylight":false,"humidity":66,"pressure":1013.210022,"temperature":12.71,"visibility":32809.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.518906,"windGust":30.6684,"temperatureApparent":8.139853,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761015600,"pressureTrend":"RISING","windDirection":266,"uvIndex":0,"windSpeed":14.9436,"snowfallIntensity":0.0},{"daylight":false,"humidity":67,"pressure":1013.330017,"temperature":12.16,"visibility":32719.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.213058,"windGust":29.07,"temperatureApparent":7.687512,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761019200,"pressureTrend":"RISING","windDirection":264,"uvIndex":0,"windSpeed":14.3208,"snowfallIntensity":0.0},{"daylight":false,"humidity":68,"pressure":1013.440002,"temperature":11.64,"visibility":32487.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.931213,"windGust":26.874001,"temperatureApparent":7.438548,"cloudCover":1,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761022800,"pressureTrend":"STEADY","windDirection":263,"uvIndex":0,"windSpeed":13.3884,"snowfallIntensity":0.0},{"daylight":false,"humidity":69,"pressure":1013.570007,"temperature":11.22,"visibility":32417.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.740585,"windGust":25.430401,"temperatureApparent":7.126996,"cloudCover":1,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761026400,"pressureTrend":"STEADY","windDirection":260,"uvIndex":0,"windSpeed":12.996,"snowfallIntensity":0.0},{"daylight":false,"humidity":70,"pressure":1013.619995,"temperature":10.8,"visibility":32285.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.545868,"windGust":26.319599,"temperatureApparent":6.737651,"cloudCover":1,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761030000,"pressureTrend":"STEADY","windDirection":255,"uvIndex":0,"windSpeed":12.8484,"snowfallIntensity":0.0},{"daylight":false,"humidity":72,"pressure":1013.799988,"temperature":10.37,"visibility":31974.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.539154,"windGust":27.4104,"temperatureApparent":6.219483,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761033600,"pressureTrend":"STEADY","windDirection":254,"uvIndex":0,"windSpeed":13.003201,"snowfallIntensity":0.0},{"daylight":false,"humidity":74,"pressure":1014.099976,"temperature":9.99,"visibility":31442.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.568222,"windGust":27.396,"temperatureApparent":5.870001,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761037200,"pressureTrend":"STEADY","windDirection":254,"uvIndex":0,"windSpeed":12.87,"snowfallIntensity":0.0},{"daylight":false,"humidity":75,"pressure":1014.380005,"temperature":9.71,"visibility":31246.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.491318,"windGust":26.218801,"temperatureApparent":5.855513,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761040800,"pressureTrend":"RISING","windDirection":254,"uvIndex":0,"windSpeed":12.121201,"snowfallIntensity":0.0},{"daylight":false,"humidity":76,"pressure":1014.659973,"temperature":9.48,"visibility":31188.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.459732,"windGust":24.0804,"temperatureApparent":5.913912,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761044400,"pressureTrend":"RISING","windDirection":251,"uvIndex":0,"windSpeed":11.350801,"snowfallIntensity":0.0},{"daylight":true,"humidity":76,"pressure":1014.859985,"temperature":9.7,"visibility":31016.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.67276,"windGust":22.258801,"temperatureApparent":8.335776,"cloudCover":3,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":8,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761048000,"pressureTrend":"RISING","windDirection":248,"uvIndex":0,"windSpeed":11.0484,"snowfallIntensity":0.0},{"daylight":true,"humidity":73,"pressure":1014.859985,"temperature":10.8,"visibility":31345.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.152725,"windGust":21.1464,"temperatureApparent":11.030827,"cloudCover":7,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":18,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761051600,"pressureTrend":"STEADY","windDirection":245,"uvIndex":1,"windSpeed":10.314,"snowfallIntensity":0.0},{"daylight":true,"humidity":67,"pressure":1014.549988,"temperature":12.3,"visibility":31755.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.346619,"windGust":21.499201,"temperatureApparent":13.069326,"cloudCover":1,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761055200,"pressureTrend":"STEADY","windDirection":237,"uvIndex":2,"windSpeed":10.173599,"snowfallIntensity":0.0},{"daylight":true,"humidity":60,"pressure":1013.969971,"temperature":14.08,"visibility":32450.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.43071,"windGust":23.270401,"temperatureApparent":14.748355,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761058800,"pressureTrend":"FALLING","windDirection":227,"uvIndex":3,"windSpeed":10.98,"snowfallIntensity":0.0},{"daylight":true,"humidity":55,"pressure":1013.330017,"temperature":15.88,"visibility":32876.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.849945,"windGust":25.5348,"temperatureApparent":16.206793,"cloudCover":1,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761062400,"pressureTrend":"FALLING","windDirection":215,"uvIndex":4,"windSpeed":12.1608,"snowfallIntensity":0.0},{"daylight":true,"humidity":51,"pressure":1012.409973,"temperature":17.190001,"visibility":32948.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.963364,"windGust":29.120398,"temperatureApparent":17.062799,"cloudCover":2,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761066000,"pressureTrend":"FALLING","windDirection":203,"uvIndex":4,"windSpeed":13.9572,"snowfallIntensity":0.0},{"daylight":true,"humidity":48,"pressure":1011.390015,"temperature":18.35,"visibility":33098.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":7.144775,"windGust":32.410801,"temperatureApparent":17.932598,"cloudCover":2,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761069600,"pressureTrend":"FALLING","windDirection":194,"uvIndex":4,"windSpeed":15.5196,"snowfallIntensity":0.0},{"daylight":true,"humidity":48,"pressure":1010.530029,"temperature":18.9,"visibility":33185.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":7.648605,"windGust":33.883198,"temperatureApparent":18.295763,"cloudCover":2,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761073200,"pressureTrend":"FALLING","windDirection":185,"uvIndex":2,"windSpeed":16.3692,"snowfallIntensity":0.0},{"daylight":true,"humidity":50,"pressure":1010.0,"temperature":19.110001,"visibility":32799.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":8.442184,"windGust":33.670799,"temperatureApparent":18.231922,"cloudCover":2,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761076800,"pressureTrend":"FALLING","windDirection":177,"uvIndex":1,"windSpeed":16.948801,"snowfallIntensity":0.0}]} """ static let nextHourWeatherJSON = """ - {"name":"NextHourForecast","metadata":{"attributionURL":"https://weatherkit.apple.com/legal-attribution.html","expireTime":"2022-11-07T17:14:51Z","language":"en-US","latitude":37.523,"longitude":-77.573,"providerName":"US National Weather Service","readTime":"2022-11-07T15:14:51Z","units":"m","version":1},"summary":[{"startTime":"2022-11-07T15:15:00Z","condition":"clear","precipitationChance":0.00,"precipitationIntensity":0.00}],"forecastStart":"2022-11-07T15:15:00Z","forecastEnd":"2022-11-07T16:38:00Z","minutes":[{"startTime":"2022-11-07T15:15:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:16:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:17:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:18:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:19:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:20:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:21:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:22:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:23:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:24:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:25:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:26:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:27:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:28:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:29:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:30:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:31:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:32:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:33:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:34:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:35:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:36:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:37:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:38:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:39:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:40:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:41:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:42:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:43:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:44:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:45:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:46:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:47:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:48:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:49:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:50:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:51:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:52:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:53:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:54:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:55:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:56:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:57:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:58:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T15:59:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:00:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:01:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:02:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:03:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:04:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:05:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:06:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:07:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:08:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:09:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:10:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:11:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:12:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:13:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:14:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:15:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:16:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:17:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:18:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:19:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:20:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:21:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:22:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:23:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:24:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:25:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:26:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:27:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:28:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:29:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:30:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:31:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:32:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:33:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:34:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:35:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:36:00Z","precipitationChance":0.00,"precipitationIntensity":0.00},{"startTime":"2022-11-07T16:37:00Z","precipitationChance":0.00,"precipitationIntensity":0.00}]} + {"metadata":{"latitude":37.541,"longitude":-77.435997,"expireTime":1761153822,"sourceType":"MODELED","attributionUrl":"https://developer.apple.com/weatherkit/data-source-attribution/","readTime":1761146622,"temporarilyUnavailable":false,"providerName":"US National Oceanic & Atmospheric Administration","reportedTime":1761146280},"condition":[{"parameters":[],"startTime":1761146580,"beginCondition":"CLEAR","forecastToken":"CLEAR","endCondition":"CLEAR"}],"summary":[{"condition":"CLEAR","precipitationChance":0,"startTime":1761146580,"precipitationIntensity":0.0}],"minutes":[{"precipitationChance":0,"startTime":1761146580,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146640,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146700,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146760,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146820,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146880,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146940,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147000,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147060,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147120,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147180,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147240,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147300,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147360,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147420,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147480,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147540,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147600,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147660,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147720,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147780,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147840,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147900,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147960,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148020,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148080,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148140,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148200,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148260,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148320,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148380,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148440,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148500,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148560,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148620,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148680,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148740,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148800,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148860,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148920,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148980,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149040,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149100,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149160,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149220,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149280,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149340,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149400,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149460,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149520,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149580,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149640,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149700,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149760,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149820,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149880,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149940,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150000,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150060,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150120,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150180,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150240,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150300,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150360,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150420,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150480,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150540,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150600,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150660,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150720,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150780,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150840,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150900,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150960,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151020,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151080,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151140,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151200,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151260,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151320,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151380,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151440,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151500,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151560,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151620,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0}],"forecastEnd":1761151680,"forecastStart":1761146580} """ static let alertJSON = """ - {"name":"WeatherAlertCollection","metadata":{"attributionURL":"https://weatherkit.apple.com/legal-attribution.html","expireTime":"2022-11-11T12:47:49Z","language":"en-US","latitude":37.523,"longitude":-77.573,"providerName":"National Weather Service","readTime":"2022-11-11T12:42:49Z","reportedTime":"2022-11-11T12:42:49Z","version":1},"detailsUrl":"https://weatherkit.apple.com/alertDetails/index.html?ids=23940bcb-2378-5ae6-99f7-30bafb62c6d8&lang=en-US&timezone=America/New_York","alerts":[{"name":"WeatherAlertSummary","id":"23940bcb-2378-5ae6-99f7-30bafb62c6d8","areaId":"51041","areaName":"Chesterfield","attributionURL":"https://alerts.weather.gov/cap/wwacapget.php?x=VA126418250E84.TornadoWatch.126418266900VA.WNSWOU9.ef29713586756b224c5cbba3ab3cfefa","countryCode":"US","description":"Tornado Watch","effectiveTime":"2022-11-11T11:15:00Z","expireTime":"2022-11-11T20:00:00Z","issuedTime":"2022-11-11T11:13:00Z","eventOnsetTime":"2022-11-11T11:15:00Z","eventEndTime":"2022-11-11T20:00:00Z","detailsUrl":"https://weatherkit.apple.com/alertDetails/index.html?ids=23940bcb-2378-5ae6-99f7-30bafb62c6d8&lang=en-US&timezone=America/New_York","phenomenon":"Tornado","precedence":0,"severity":"severe","source":"National Weather Service","eventSource":"US","urgency":"expected","certainty":"likely","importance":"high","responses":[]}]} + {"metadata":{"language":"en","latitude":37.541,"longitude":-77.435997,"expireTime":1761146922,"sourceType":"STATION","attributionUrl":"https://developer.apple.com/weatherkit/data-source-attribution/","readTime":1761146622,"temporarilyUnavailable":false,"providerName":"National Weather Service","reportedTime":1761115740},"alerts":[{"id":"9b2651f9-dd76-5874-beac-98c5a8465d4a","description":"Special Weather Statement","token":"SPECIAL_WEATHER_STATEMENT","phenomenon":"Other","severity":"MODERATE","significance":"UNKNOWN","source":"National Weather Service","urgency":"EXPECTED","certainty":"OBSERVED","importance":"NORMAL","responses":["EXECUTE"],"eventOnsetTime":1761115740,"detailsUrl":"https://weatherkit.apple.com/alertDetails/index.html?ids=9b2651f9-dd76-5874-beac-98c5a8465d4a&lang=en-US&timezone=America/New_York","effectiveTime":1761115740,"issuedTime":1761115740,"eventSource":"US","areaId":"vaz515","expireTime":1761174000,"countryCode":"US","attributionUrl":"https://www.weather.gov"}],"detailsUrl":"https://weatherkit.apple.com/alertDetails/index.html?ids=9b2651f9-dd76-5874-beac-98c5a8465d4a&lang=en-US&timezone=America/New_York"} """ } From ff63eecfe838df0f167f6b926a9ce1f95285913c Mon Sep 17 00:00:00 2001 From: Jeremy Greenwood Date: Tue, 28 Oct 2025 11:49:30 -0400 Subject: [PATCH 03/11] Dev tooling commit 70770bb4b1e395ff49edc35d62847adb1c2208f7 Author: Jeremy Greenwood Date: Thu Oct 23 14:56:09 2025 -0400 swiftlint commit 3d16c1688c43cd3e1423c69ed816fe47df5b8a13 Author: Jeremy Greenwood Date: Thu Oct 23 14:55:59 2025 -0400 update container tools commit f44a797429de970029b37e9286eba499ef945f33 Author: Jeremy Greenwood Date: Thu Feb 20 11:26:03 2025 -0500 Enable Swift 6 Mode (#38) --- .devcontainer/devcontainer.json | 13 +- .gitignore | 1 + .mise.toml | 6 + .swiftlint.yml | 76 +++++++ CLAUDE.md | 199 ++++++++++++++++++ Package.swift | 10 +- README.md | 3 + .../Public/Celestial/MoonEvents.swift | 2 +- .../Public/Celestial/MoonPhase.swift | 2 +- .../Public/Celestial/SunEvents.swift | 2 +- .../Public/Characteristics/AlertSummary.swift | 2 +- .../Public/Characteristics/AlertUrgency.swift | 2 +- .../Public/Characteristics/Certainty.swift | 2 +- .../Characteristics/Precipitation.swift | 2 +- .../Characteristics/PressureTrend.swift | 2 +- .../Public/Characteristics/UVIndex.swift | 2 +- .../Characteristics/WeatherCondition.swift | 2 +- .../Characteristics/WeatherSeverity.swift | 2 +- .../Public/Characteristics/Wind.swift | 2 +- .../Public/Forecast/DayWeather.swift | 2 +- .../Public/Forecast/Forecast.swift | 2 +- .../Public/Forecast/HourWeather.swift | 2 +- .../Public/Forecast/MinuteWeather.swift | 2 +- .../Public/Forecast/WeatherAlert.swift | 2 +- .../Public/Forecast/WeatherAvailability.swift | 2 +- .../Public/Protocols/LocationProtocol.swift | 4 +- .../Public/Requests/CurrentWeather.swift | 2 +- .../Public/Requests/WeatherAttribution.swift | 2 +- .../Public/Requests/WeatherMetadata.swift | 2 +- .../Public/Requests/WeatherQuery.swift | 2 +- Sources/OpenWeatherKit/Public/Weather.swift | 2 +- .../OpenWeatherKit/Public/WeatherError.swift | 2 +- .../Public/WeatherService.swift | 4 +- scripts/post_create_container.sh | 15 ++ scripts/setup_local_dev.sh | 48 +++++ 35 files changed, 388 insertions(+), 39 deletions(-) create mode 100644 .mise.toml create mode 100644 .swiftlint.yml create mode 100644 CLAUDE.md create mode 100755 scripts/post_create_container.sh create mode 100755 scripts/setup_local_dev.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c7e2437..07425e5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,18 +1,19 @@ { "name": "Swift", - "image": "swift:5.10", + "image": "swift:6.1", "features": { "ghcr.io/devcontainers/features/common-utils:2": { "installZsh": "false", "username": "vscode", - "userUid": "1000", - "userGid": "1000", + "userUid": "1001", + "userGid": "1001", "upgradePackages": "false" }, "ghcr.io/devcontainers/features/git:1": { "version": "os-provided", "ppa": "false" - } + }, + "ghcr.io/devcontainers/features/node:1": { } }, "runArgs": [ "--cap-add=SYS_PTRACE", @@ -29,7 +30,7 @@ }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ - "sswg.swift-lang" + "swiftlang.swift-vscode" ] } }, @@ -37,7 +38,7 @@ // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "swift --version", + "postCreateCommand": "./scripts/post_create_container.sh", // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode" diff --git a/.gitignore b/.gitignore index f8ae50f..2ea1124 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc .vscode +.claude* diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..0e44b1a --- /dev/null +++ b/.mise.toml @@ -0,0 +1,6 @@ +[tools] +swiftlint = "latest" +claude = "latest" + +[settings] +experimental = true \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..6cc81a1 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,76 @@ +included: + - Sources + - Package.swift + +excluded: + - .build + - Submodules + +opt_in_rules: + - array_init + - attributes + - closure_end_indentation + - closure_spacing + - empty_count + - explicit_init + - extension_access_modifier + - fatal_error_message + - first_where + - let_var_whitespace + - literal_expression_end_indentation + - nimble_operator + - operator_usage_whitespace + - overridden_super_call + - pattern_matching_keywords + - private_outlet + - prohibited_super_call + - redundant_nil_coalescing + - unneeded_parentheses_in_closure_argument + - vertical_parameter_alignment_on_call + +disabled_rules: + - attributes + - multiple_closures_with_trailing_closure + - trailing_comma + - vertical_parameter_alignment_on_call + - void_return + +custom_rules: + disable_print: + name: "print usage" + regex: "((\\bprint)|(Swift\\.print))\\s*\\(" + message: "User app.logger or request.logger instead of print" + severity: warning + +# Default Rule Configuration +type_name: + min_length: 2 + +identifier_name: + min_length: 2 + allowed_symbols: "_" + excluded: + - x + - f + - i + +file_length: + warning: 500 + error: 1000 + +function_body_length: 50 + +line_length: 200 + +cyclomatic_complexity: + ignores_case_statements: true + +large_tuple: + warning: 6 + error: 10 + +nesting: + type_level: + warning: 2 + function_level: + warning: 10 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..491431a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,199 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +OpenWeatherKit is a Swift wrapper around the WeatherKit REST API, bringing native Swift WeatherKit functionality to platforms Apple doesn't currently support (particularly Linux). The API mirrors Apple's WeatherKit as closely as possible. + +## Build and Test Commands + +### Building +```bash +swift build +``` + +### Running Tests +```bash +swift test +``` + +### Running a Single Test +```bash +swift test --filter . +# Example: swift test --filter OpenWeatherKitTests.testWeather +``` + +### Platform-Specific Testing +The test suite has conditional compilation for Apple platforms vs Linux: +- Apple platforms: Tests include CoreLocation-based geocoding +- Linux: Tests require explicit countryCode and timezone parameters + +## Architecture + +### Core Request Flow + +1. **WeatherService** (public API) - Entry point for all weather requests + - Accepts `LocationProtocol` (latitude/longitude) + - Variadic methods support 1-6 different `WeatherQuery` datasets in a single call + - On Apple platforms: automatically geocodes location to get country code and timezone + - On Linux: requires explicit `countryCode` and `timezone` parameters + +2. **NetworkClient** (internal) - HTTP layer + - Builds URLs via `Route` enum + - Handles JWT bearer token authentication + - Uses `URLSession` on Apple platforms, `AsyncHTTPClient` on Linux + - Fetches data in parallel using `TaskGroup` when multiple queries are requested + - Returns `WeatherProxy` which aggregates partial results + +3. **WeatherProxy** (internal) - Result aggregation + - Container for all possible weather data types + - Supports combining multiple partial proxies (for parallel fetches) + - Maps from API models to public models + +4. **WeatherQuery** (public) - Type-safe dataset requests + - Generic query structure with static factory methods (`.current`, `.daily`, `.hourly`, etc.) + - Contains both the query type and a closure to extract results from `WeatherProxy` + - Supports updating with country codes for alerts/availability queries + +### Platform Differences + +**Apple Platforms** (`#if canImport(CoreLocation)`): +- Use `URLSession` for networking +- Include `Geocoder` for automatic country code/timezone resolution +- Support simplified API without explicit country code + +**Linux** (`#if os(Linux)`): +- Use `AsyncHTTPClient` from swift-server +- Require explicit `countryCode` and `timezone` on all requests +- `WeatherService` includes `shutdown()` method to clean up HTTP client + +### Data Flow + +``` +API Response (APIWeather/APIForecastDaily/etc.) + ↓ (via extension +Map.swift files) +WeatherProxy (aggregates partial results) + ↓ (via WeatherQuery.result closure) +Public Models (CurrentWeather/Forecast/etc.) +``` + +### Internal vs Public Separation + +- **Internal/Models**: API response models prefixed with `API*` (e.g., `APIWeather`, `APICurrentWeather`) +- **Internal/Extensions**: Mapping logic from API models to public models (`+Map.swift` files) +- **Public**: User-facing types matching Apple's WeatherKit API + +### Key Protocols + +- **LocationProtocol**: Abstraction for any type with `latitude` and `longitude` +- **Client**: Protocol for HTTP clients (implemented by `URLSession` wrapper and `AsyncHTTPClient` wrapper) + +## Testing + +Tests use a `MockClient` that returns predefined JSON responses from `MockData`. The mock client: +- Accepts an `Include` set to control which datasets to return +- Simulates API responses without real network calls +- On Apple platforms, uses `Geocoder.mock` for testing geocoding + +## Dependencies + +- **Swift 5.9+** minimum +- **No dependencies** on Apple platforms (uses `URLSession`) +- **AsyncHTTPClient** dependency on Linux (conditionally added via `#if os(Linux)` in Package.swift) + +## JWT Authentication + +The library does NOT generate JWTs. Users must provide a closure that returns a valid JWT string when initializing `WeatherService.Configuration`. The README recommends using Vapor's `jwt-kit` for JWT generation. + +Required JWT claims: +- `exp` (expiration) +- `iat` (issued at) +- `iss` (issuer - Team ID) +- `sub` (subject - Service Identifier) +- Must be signed with ES256 using the private key from Apple Developer Portal +- Must include Key ID in header + +## API Endpoint Structure + +Base URL: `https://weatherkit.apple.com` + +- Weather: `/api/v2/weather/{language}/{latitude}/{longitude}?dataSets=...&timezone=...` +- Availability: `/api/v2/availability/{latitude}/{longitude}?country={countryCode}` + +## Date Handling + +- JSON responses use Unix epoch timestamps (`secondsSince1970`) +- JSONDecoder configured with `.dateDecodingStrategy = .secondsSince1970` +- Date extensions provide utilities like `.hoursFromNow(24)` and `.daysFromNow(10)` + +## Coding Conventions + +- `@usableFromInline` on internal types/methods that are called from `@inlinable` public APIs +- Extensive use of `@inlinable` on public API surface for performance +- Sendable conformance throughout for Swift 6 compatibility +- TextCaseCoding for snake_case/camelCase conversion between API and Swift + +## SwiftLint Integration + +### Automatic Linting Hook + +This repository includes a PostToolUse hook that automatically runs SwiftLint when Claude Code edits Swift files. The hook: +- Runs after every Edit or Write operation on Swift files +- Automatically fixes correctable violations (trailing whitespace, formatting issues, etc.) +- Reports remaining violations back to Claude for resolution +- Only processes Swift files in `Sources/`, `Tests/`, or `Package.swift` +- Completes within 15 seconds (timeout protection) + +### Setup Requirements + +**Required:** SwiftLint 0.50.0 or later must be installed: +```bash +brew install swiftlint +``` + +**Activation:** Copy the example settings to enable the hook: +```bash +cp .claude/settings.example.json .claude/settings.local.json +``` + +The hook is configured in `.claude/settings.local.json` (gitignored for local flexibility). + +### Hook Behavior + +**When you edit a Swift file:** +1. Hook receives the file path from Claude Code +2. Validates the file is a Swift file in target directories +3. Runs `swiftlint --fix` to auto-correct violations +4. Runs `swiftlint lint` to check for remaining violations +5. If violations remain: Reports them to Claude with file:line:column references and blocks the edit +6. If no violations: Completes silently + +**Example violation report:** +``` +SwiftLint found 2 violation(s) in Sources/OpenWeatherKit/Public/Weather.swift: +Sources/OpenWeatherKit/Public/Weather.swift:42:1: warning: Line should be 200 characters or less (line_length) +Sources/OpenWeatherKit/Public/Weather.swift:55:10: error: Print statement must not be used (custom_rules) +``` + +### Performance Impact + +- Adds approximately 0.2-0.6 seconds per Swift file edit +- Runs only on Swift files (non-Swift files pass through instantly) +- Timeout protection prevents indefinite blocking + +### Disabling the Hook + +To disable SwiftLint integration: +```bash +rm .claude/settings.local.json +``` + +Or comment out the hook configuration in the settings file. + +### Missing SwiftLint + +If SwiftLint is not installed, the hook will: +- Exit gracefully with a warning message +- NOT block edits +- Suggest installation with `brew install swiftlint` diff --git a/Package.swift b/Package.swift index 736217f..f9d5c82 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.5 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -10,7 +10,8 @@ let package = Package( .iOS(.v13), .watchOS(.v6), .tvOS(.v13), - .macOS(.v11) + .macOS(.v11), + .visionOS(.v1) ], products: [ .library( @@ -27,12 +28,11 @@ let package = Package( .testTarget( name: "OpenWeatherKitTests", dependencies: ["OpenWeatherKit"]), - ] + ], + swiftLanguageVersions: [.version("6"), .v5] ) #if os(Linux) package.dependencies.append(.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.19.0")) package.targets.first { $0.name == "OpenWeatherKit" }?.dependencies.append(.product(name: "AsyncHTTPClient", package: "async-http-client")) #endif - - diff --git a/README.md b/README.md index 4a47702..887117b 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,12 @@ is nearly identical to Apple's [WeatherKit](https://developer.apple.com/document ## 💻 Supported Platforms +Minimum Swift version of 5.9 + - iOS 13+ - watchOS 6+ - tvOS 13+ +- visionOS 1+ - macOS 11+ - Ubuntu 18.04+ diff --git a/Sources/OpenWeatherKit/Public/Celestial/MoonEvents.swift b/Sources/OpenWeatherKit/Public/Celestial/MoonEvents.swift index ea66d6c..437865a 100644 --- a/Sources/OpenWeatherKit/Public/Celestial/MoonEvents.swift +++ b/Sources/OpenWeatherKit/Public/Celestial/MoonEvents.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that represents lunar events. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct MoonEvents: Sendable { /// The moon phase. diff --git a/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift b/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift index 55b1409..917030e 100644 --- a/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift +++ b/Sources/OpenWeatherKit/Public/Celestial/MoonPhase.swift @@ -8,7 +8,7 @@ import Foundation /// An enumeration that specifies the moon phase kind. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @frozen public enum MoonPhase : String, CustomStringConvertible, CaseIterable { enum CodingKeys: String, CodingKey { case new, waxingCrescent, firstQuarter, waxingGibbous, full, waningGibbous, waningCrescent diff --git a/Sources/OpenWeatherKit/Public/Celestial/SunEvents.swift b/Sources/OpenWeatherKit/Public/Celestial/SunEvents.swift index 1719e0c..80b50e7 100644 --- a/Sources/OpenWeatherKit/Public/Celestial/SunEvents.swift +++ b/Sources/OpenWeatherKit/Public/Celestial/SunEvents.swift @@ -8,7 +8,7 @@ import Foundation /// An enumeration that represents dates of solar events, including sunrise, sunset, dawn, and dusk. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct SunEvents: Sendable { /// The time of astronomical sunrise when the sun’s center is 18° below the horizon. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift b/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift index 0e49eb3..9d36a1b 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift @@ -8,7 +8,7 @@ import Foundation /// All information related to the weather alert -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct AlertSummary: Codable, Equatable, Sendable { public var name: String public var id: String diff --git a/Sources/OpenWeatherKit/Public/Characteristics/AlertUrgency.swift b/Sources/OpenWeatherKit/Public/Characteristics/AlertUrgency.swift index 67b5026..288e586 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/AlertUrgency.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/AlertUrgency.swift @@ -8,7 +8,7 @@ import Foundation /// The urgency of the weather alert -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum AlertUrgency: String, Codable, Equatable, Sendable { // Take responsive action immediately. case immediate diff --git a/Sources/OpenWeatherKit/Public/Characteristics/Certainty.swift b/Sources/OpenWeatherKit/Public/Characteristics/Certainty.swift index c0b5935..70b42cb 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/Certainty.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/Certainty.swift @@ -8,7 +8,7 @@ import Foundation /// The likelihood the weather alert event will happen -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum Certainty: String, Codable, Equatable, Sendable { // The event has already occurred or is ongoing. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift b/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift index 37b4df5..1661a15 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/Precipitation.swift @@ -8,7 +8,7 @@ import Foundation /// The form of precipitation. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum Precipitation : String, CaseIterable, CustomStringConvertible, Hashable, Sendable { /// No precipitation. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift b/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift index 47d1237..062e3ca 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/PressureTrend.swift @@ -8,7 +8,7 @@ import Foundation /// The atmospheric pressure change over time. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum PressureTrend : String, CaseIterable, CustomStringConvertible, Hashable, Sendable { /// The pressure is rising. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift b/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift index 0736329..1c10414 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift @@ -8,7 +8,7 @@ import Foundation /// The expected intensity of ultraviolet radiation from the sun. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct UVIndex: Sendable { /// The UV Index value. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift b/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift index 442d10a..c993952 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift @@ -8,7 +8,7 @@ import Foundation /// A description of the current weather condition. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum WeatherCondition : String, CaseIterable, CustomStringConvertible, Hashable, Sendable { /// The kind of condition. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift b/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift index 1606846..5bc71ff 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/WeatherSeverity.swift @@ -8,7 +8,7 @@ import Foundation /// A description of the severity of the severe weather event. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum WeatherSeverity : String, Codable, CaseIterable, CustomStringConvertible, Hashable, Sendable { /// Specifies "minimal" or no threat to life or property. diff --git a/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift b/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift index ca5dc02..785229c 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/Wind.swift @@ -8,7 +8,7 @@ import Foundation /// Contains wind data of speed, direction, and gust. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Wind: Sendable { /// General indicator of wind direction, often referred to as "due north", "due south", etc. diff --git a/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift b/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift index 52e694e..9f1e454 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/DayWeather.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that represents the weather conditions for the day. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct DayWeather: Sendable { /// The start date of the day weather. diff --git a/Sources/OpenWeatherKit/Public/Forecast/Forecast.swift b/Sources/OpenWeatherKit/Public/Forecast/Forecast.swift index d8443f4..b9559c4 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/Forecast.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/Forecast.swift @@ -8,7 +8,7 @@ import Foundation /// A forecast collection for minute, hourly, and daily forecasts. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Forecast : RandomAccessCollection, Codable, Equatable, Sendable where Element : Decodable, Element : Encodable, Element : Equatable, Element : Sendable { public init(forecast: [Element], metadata: WeatherMetadata) { self.forecast = forecast diff --git a/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift b/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift index de59043..373a248 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/HourWeather.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that represents the weather conditions for the hour. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct HourWeather: Sendable { /// The start date of the hour weather. diff --git a/Sources/OpenWeatherKit/Public/Forecast/MinuteWeather.swift b/Sources/OpenWeatherKit/Public/Forecast/MinuteWeather.swift index ccd9753..745a5d9 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/MinuteWeather.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/MinuteWeather.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that represents the next hour minute forecasts. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct MinuteWeather: Sendable { /// The start date of the minute weather. diff --git a/Sources/OpenWeatherKit/Public/Forecast/WeatherAlert.swift b/Sources/OpenWeatherKit/Public/Forecast/WeatherAlert.swift index 65b98bd..42bf854 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/WeatherAlert.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/WeatherAlert.swift @@ -8,7 +8,7 @@ import Foundation /// A weather alert issued for the requested location by a governmental authority. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct WeatherAlert: Sendable { /// The site for more details about the weather alert. Required link for attribution. public var detailsURL: URL diff --git a/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift b/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift index e30557f..fc05167 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that indicates the availability of data at the requested location. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct WeatherAvailability: Sendable { /// The minute forecast availability. diff --git a/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift b/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift index 69f273e..609ab20 100644 --- a/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift +++ b/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift @@ -16,7 +16,7 @@ extension CLLocation: LocationProtocol, @unchecked Sendable { #endif /// Defines the interface for a location -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public protocol LocationProtocol: Sendable { var latitude: Double { get } var longitude: Double { get } @@ -24,7 +24,7 @@ public protocol LocationProtocol: Sendable { init(latitude: Double, longitude: Double) } -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Location: LocationProtocol, Equatable, Codable, Sendable { public let latitude: Double public let longitude: Double diff --git a/Sources/OpenWeatherKit/Public/Requests/CurrentWeather.swift b/Sources/OpenWeatherKit/Public/Requests/CurrentWeather.swift index 21bb3f7..7444b00 100644 --- a/Sources/OpenWeatherKit/Public/Requests/CurrentWeather.swift +++ b/Sources/OpenWeatherKit/Public/Requests/CurrentWeather.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that describes the current conditions observed at a location. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct CurrentWeather: Sendable { public init( date: Date, diff --git a/Sources/OpenWeatherKit/Public/Requests/WeatherAttribution.swift b/Sources/OpenWeatherKit/Public/Requests/WeatherAttribution.swift index f9c4527..fc9a93d 100644 --- a/Sources/OpenWeatherKit/Public/Requests/WeatherAttribution.swift +++ b/Sources/OpenWeatherKit/Public/Requests/WeatherAttribution.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that defines the necessary information for attributing a weather data provider. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct WeatherAttribution { /// The weather data provider name. diff --git a/Sources/OpenWeatherKit/Public/Requests/WeatherMetadata.swift b/Sources/OpenWeatherKit/Public/Requests/WeatherMetadata.swift index 3f79ef0..8738022 100644 --- a/Sources/OpenWeatherKit/Public/Requests/WeatherMetadata.swift +++ b/Sources/OpenWeatherKit/Public/Requests/WeatherMetadata.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that provides additional weather information. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct WeatherMetadata: Sendable { /// The date of the weather data request. diff --git a/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift b/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift index fbde30c..d537b03 100644 --- a/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift +++ b/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates a generic weather dataset request. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct WeatherQuery { @usableFromInline let queryType: QueryType diff --git a/Sources/OpenWeatherKit/Public/Weather.swift b/Sources/OpenWeatherKit/Public/Weather.swift index c19b9a7..dde31b9 100644 --- a/Sources/OpenWeatherKit/Public/Weather.swift +++ b/Sources/OpenWeatherKit/Public/Weather.swift @@ -8,7 +8,7 @@ import Foundation /// A model representing the aggregate weather data the caller requests. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Weather: Sendable { /// The current weather forecast. diff --git a/Sources/OpenWeatherKit/Public/WeatherError.swift b/Sources/OpenWeatherKit/Public/WeatherError.swift index e045f4a..0f79dbc 100644 --- a/Sources/OpenWeatherKit/Public/WeatherError.swift +++ b/Sources/OpenWeatherKit/Public/WeatherError.swift @@ -8,7 +8,7 @@ import Foundation /// An error WeatherKit returns. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public enum WeatherError : LocalizedError, Equatable, Hashable { /// Could not find country code case countryCode diff --git a/Sources/OpenWeatherKit/Public/WeatherService.swift b/Sources/OpenWeatherKit/Public/WeatherService.swift index de3b210..bf213b1 100644 --- a/Sources/OpenWeatherKit/Public/WeatherService.swift +++ b/Sources/OpenWeatherKit/Public/WeatherService.swift @@ -15,11 +15,11 @@ import CoreLocation #endif /// Provides an interface for obtaining weather data. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) final public class WeatherService: Sendable { /// Establishes the configuration for weather requests. - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Configuration: Sendable { public enum Language: String, Sendable { diff --git a/scripts/post_create_container.sh b/scripts/post_create_container.sh new file mode 100755 index 0000000..223fc63 --- /dev/null +++ b/scripts/post_create_container.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +/workspaces/open-weather-kit/scripts/setup_local_dev.sh + +SETTINGS_PATH="/home/vscode/.vscode-server/data/Machine/settings.json" +SWIFTLINT_KEY="swiftlint.path" +SWIFTLINT_VALUE="/home/vscode/.local/share/mise/installs/swiftlint/latest/swiftlint" + +# Read the JSON string from the file +SETTINGS_FILE=$(cat "$SETTINGS_PATH") +# Update the value of the key +UPDATED_SETTINGS=$(echo "$SETTINGS_FILE" | jq --arg key "$SWIFTLINT_KEY" --arg value "$SWIFTLINT_VALUE" '.[$key] = $value') + +echo "$UPDATED_SETTINGS" > "$SETTINGS_PATH" \ No newline at end of file diff --git a/scripts/setup_local_dev.sh b/scripts/setup_local_dev.sh new file mode 100755 index 0000000..ededc69 --- /dev/null +++ b/scripts/setup_local_dev.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# detect whether default shell is bash, zsh, or fish and active mise +activate_mise() { + echo "Activating mise..." + if [[ $SHELL == *"bash"* ]]; then + echo 'Activating mise for bash' + echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc + elif [[ $SHELL == *"zsh"* ]]; then + echo 'Activating mise for zsh' + echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc + elif [[ $SHELL == *"fish"* ]]; then + echo 'Activating mise for fish' + echo '~/.local/bin/mise activate fish | source' >> ~/.config/fish/config.fish + else + echo "Shell not supported. Please set up manually." + exit 1 + fi + echo "mise activated" +} + +# detect whether mise is installed +if [ -z "$(which mise)" ]; then + echo "mise is not installed. Installing mise..." + curl https://mise.run | sh +fi + +activate_mise + +echo "Enabling experimental mise features..." +mise settings set experimental true + +# install dependencies with mise +echo "Installing dependencies..." +mise trust +mise install + +# mise run migrate +# mise run seed + +cat << EOF + +Setup is complete. You may need to source mise to use the installed dependencies. +bash: source ~/.bashrc +zsh: source ~/.zshrc +fish: source ~/.config/fish/config.fish + +EOF \ No newline at end of file From 0df1c08732d1bc7bb63723b911af3070b06307b6 Mon Sep 17 00:00:00 2001 From: Jeremy Greenwood Date: Thu, 30 Oct 2025 17:13:14 -0400 Subject: [PATCH 04/11] Statistics Endpoint Implementation (#41) --- .../Extensions/APIDailyStatistics+Map.swift | 72 +++ .../Extensions/APIDailySummary+Map.swift | 60 +++ .../Extensions/APIHourlyStatistics+Map.swift | 48 ++ .../Extensions/APIMonthlyStatistics+Map.swift | 71 +++ .../Internal/Extensions/Date+Utils.swift | 72 +++ .../Extensions/DateFormatter+Utils.swift | 30 ++ .../Internal/Extensions/Sendable+Compat.swift | 21 - .../Internal/Models/APIDailySummary.swift | 42 ++ .../Internal/Models/APIStatistics.swift | 122 ++++++ .../Internal/NetworkClient.swift | 100 +++++ Sources/OpenWeatherKit/Internal/Route.swift | 18 +- .../Internal/StatisticsQuery.swift | 40 ++ .../Public/Protocols/LocationProtocol.swift | 2 +- .../DailyWeatherStatisticsQuery.swift | 30 ++ .../Requests/DailyWeatherSummaryQuery.swift | 30 ++ .../HourlyWeatherStatisticsQuery.swift | 25 ++ .../MonthlyWeatherStatisticsQuery.swift | 30 ++ .../Statistics/DailyWeatherStatistics.swift | 41 ++ .../DayPrecipitationStatistics.swift | 26 ++ .../Statistics/DayTemperatureStatistics.swift | 33 ++ .../HourTemperatureStatistics.swift | 20 + .../Statistics/HourlyWeatherStatistics.swift | 41 ++ .../MonthPrecipitationStatistics.swift | 26 ++ .../MonthTemperatureStatistics.swift | 24 + .../Statistics/MonthlyWeatherStatistics.swift | 41 ++ .../Public/Statistics/Percentiles.swift | 24 + .../Public/Summary/DailyWeatherSummary.swift | 40 ++ .../Summary/DayPrecipitationSummary.swift | 24 + .../Summary/DayTemperatureSummary.swift | 24 + .../OpenWeatherKit/Public/WeatherError.swift | 7 + .../Public/WeatherService+Forecast.swift | 414 ++++++++++++++++++ .../Public/WeatherService+Statistics.swift | 401 +++++++++++++++++ .../Public/WeatherService+Summary.swift | 164 +++++++ .../Public/WeatherService.swift | 401 ----------------- .../WeatherServiceDateRangeTests.swift | 299 +++++++++++++ 35 files changed, 2438 insertions(+), 425 deletions(-) create mode 100644 Sources/OpenWeatherKit/Internal/Extensions/APIDailyStatistics+Map.swift create mode 100644 Sources/OpenWeatherKit/Internal/Extensions/APIDailySummary+Map.swift create mode 100644 Sources/OpenWeatherKit/Internal/Extensions/APIHourlyStatistics+Map.swift create mode 100644 Sources/OpenWeatherKit/Internal/Extensions/APIMonthlyStatistics+Map.swift create mode 100644 Sources/OpenWeatherKit/Internal/Extensions/DateFormatter+Utils.swift delete mode 100644 Sources/OpenWeatherKit/Internal/Extensions/Sendable+Compat.swift create mode 100644 Sources/OpenWeatherKit/Internal/Models/APIDailySummary.swift create mode 100644 Sources/OpenWeatherKit/Internal/Models/APIStatistics.swift create mode 100644 Sources/OpenWeatherKit/Internal/StatisticsQuery.swift create mode 100644 Sources/OpenWeatherKit/Public/Requests/DailyWeatherStatisticsQuery.swift create mode 100644 Sources/OpenWeatherKit/Public/Requests/DailyWeatherSummaryQuery.swift create mode 100644 Sources/OpenWeatherKit/Public/Requests/HourlyWeatherStatisticsQuery.swift create mode 100644 Sources/OpenWeatherKit/Public/Requests/MonthlyWeatherStatisticsQuery.swift create mode 100644 Sources/OpenWeatherKit/Public/Statistics/DailyWeatherStatistics.swift create mode 100644 Sources/OpenWeatherKit/Public/Statistics/DayPrecipitationStatistics.swift create mode 100644 Sources/OpenWeatherKit/Public/Statistics/DayTemperatureStatistics.swift create mode 100644 Sources/OpenWeatherKit/Public/Statistics/HourTemperatureStatistics.swift create mode 100644 Sources/OpenWeatherKit/Public/Statistics/HourlyWeatherStatistics.swift create mode 100644 Sources/OpenWeatherKit/Public/Statistics/MonthPrecipitationStatistics.swift create mode 100644 Sources/OpenWeatherKit/Public/Statistics/MonthTemperatureStatistics.swift create mode 100644 Sources/OpenWeatherKit/Public/Statistics/MonthlyWeatherStatistics.swift create mode 100644 Sources/OpenWeatherKit/Public/Statistics/Percentiles.swift create mode 100644 Sources/OpenWeatherKit/Public/Summary/DailyWeatherSummary.swift create mode 100644 Sources/OpenWeatherKit/Public/Summary/DayPrecipitationSummary.swift create mode 100644 Sources/OpenWeatherKit/Public/Summary/DayTemperatureSummary.swift create mode 100644 Sources/OpenWeatherKit/Public/WeatherService+Forecast.swift create mode 100644 Sources/OpenWeatherKit/Public/WeatherService+Statistics.swift create mode 100644 Sources/OpenWeatherKit/Public/WeatherService+Summary.swift create mode 100644 Tests/OpenWeatherKitTests/WeatherServiceDateRangeTests.swift diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIDailyStatistics+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIDailyStatistics+Map.swift new file mode 100644 index 0000000..d9052c8 --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIDailyStatistics+Map.swift @@ -0,0 +1,72 @@ +// +// APIDailyStatistics+Map.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension APIDailyStatistics { + var dailyPrecipitationStatistics: DailyWeatherStatistics { + DailyWeatherStatistics( + days: days.map(\.precipitationStatistics), + baselineStartDate: Date.daysFromEpoch(baselineStart), + metadata: metadata.weatherMetadata + ) + } + + var dailyTemperatureStatistics: DailyWeatherStatistics { + DailyWeatherStatistics( + days: days.map(\.temperatureStatistics), + baselineStartDate: Date.daysFromEpoch(baselineStart), + metadata: metadata.weatherMetadata + ) + } + + @usableFromInline + func parse(query: DailyWeatherStatisticsQuery) -> DailyWeatherStatistics { + switch query.statisticsType { + case .temperature: + guard let stats = dailyTemperatureStatistics as? DailyWeatherStatistics else { + preconditionFailure("Type mismatch: expected DailyWeatherStatistics but got DailyWeatherStatistics<\(T.self)>") + } + return stats + case .precipitation: + guard let stats = dailyPrecipitationStatistics as? DailyWeatherStatistics else { + preconditionFailure("Type mismatch: expected DailyWeatherStatistics but got DailyWeatherStatistics<\(T.self)>") + } + return stats + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension APIDailyStatisticsData { + var precipitationStatistics: DayPrecipitationStatistics { + guard let precipitation else { + preconditionFailure("Precipitation is nil, this could be a logic error") + } + + return DayPrecipitationStatistics( + day: dayOfYear, + averagePrecipitationProbability: Double(precipitation.probability), + averagePrecipitationAmount: Measurement(value: precipitation.averageAmount, unit: .millimeters), + averageSnowfallAmount: Measurement(value: precipitation.averageSnowfallAmount, unit: .millimeters) + ) + } + + var temperatureStatistics: DayTemperatureStatistics { + guard let temperature else { + preconditionFailure("Temperature is nil, this could be a logic error") + } + + return DayTemperatureStatistics( + day: dayOfYear, + averageLowTemperature: Measurement(value: temperature.min ?? .nan, unit: .celsius), + averageHighTemperature: Measurement(value: temperature.max ?? .nan, unit: .celsius) + ) + } +} + diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIDailySummary+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIDailySummary+Map.swift new file mode 100644 index 0000000..b0d1356 --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIDailySummary+Map.swift @@ -0,0 +1,60 @@ +// +// APIDailySummary+Map.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension APIDailySummary { + var dailyPrecipitationSummary: DailyWeatherSummary { + DailyWeatherSummary( + days: days.map(\.precipitationSummary), + metadata: metadata.weatherMetadata + ) + } + + var dailyTemperatureSummary: DailyWeatherSummary { + DailyWeatherSummary( + days: days.map(\.temperatureSummary), + metadata: metadata.weatherMetadata + ) + } + + @usableFromInline + func parse(query: DailyWeatherSummaryQuery) -> DailyWeatherSummary { + switch query.statisticsType { + case .temperature: + guard let stats = dailyTemperatureSummary as? DailyWeatherSummary else { + preconditionFailure("Type mismatch: expected DailyWeatherSummary but got DailyWeatherSummary<\(T.self)>") + } + return stats + case .precipitation: + guard let stats = dailyPrecipitationSummary as? DailyWeatherSummary else { + preconditionFailure("Type mismatch: expected DailyWeatherSummary but got DailyWeatherSummary<\(T.self)>") + } + return stats + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension APIDailySummaryDay { + var precipitationSummary: DayPrecipitationSummary { + DayPrecipitationSummary( + date: Date.daysFromEpoch(date), + precipitationAmount: Measurement(value: precipitationAmount, unit: .millimeters), + snowfallAmount: Measurement(value: snowfallAmount, unit: .millimeters) + ) + } + + var temperatureSummary: DayTemperatureSummary { + DayTemperatureSummary( + date: Date.daysFromEpoch(date), + lowTemperature: Measurement(value: temperatureMin, unit: .celsius), + highTemperature: Measurement(value: temperatureMax, unit: .celsius) + ) + } +} diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIHourlyStatistics+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIHourlyStatistics+Map.swift new file mode 100644 index 0000000..e665dbc --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIHourlyStatistics+Map.swift @@ -0,0 +1,48 @@ +// +// APIHourlyStatistics+Map.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension APIHourlyStatistics { + @usableFromInline + var hourlyTemperatureStatistics: HourlyWeatherStatistics { + HourlyWeatherStatistics( + hours: hours.map(\.temperatureStatistics), + baselineStartDate: Date.daysFromEpoch(baselineStart), + metadata: metadata.weatherMetadata + ) + } + + @usableFromInline + func parse(query: HourlyWeatherStatisticsQuery) -> HourlyWeatherStatistics { + switch query.statisticsType { + case .temperature: + guard let stats = hourlyTemperatureStatistics as? HourlyWeatherStatistics else { + preconditionFailure("Type mismatch: expected HourlyWeatherStatistics but got HourlyWeatherStatistics<\(T.self)>") + } + return stats + default: + preconditionFailure("Invalid statistics type: HourlyWeatherStatistics<\(T.self)>") + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension APIHourlyStatisticsData { + @usableFromInline + var temperatureStatistics: HourTemperatureStatistics { + HourTemperatureStatistics( + hour: hourOfYear, + percentiles: Percentiles( + p10: Measurement(value: temperature.p10 ?? 0.0, unit: .celsius), + p50: Measurement(value: temperature.p50 ?? 0.0, unit: .celsius), + p90: Measurement(value: temperature.p90 ?? 0.0, unit: .celsius) + ) + ) + } +} diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIMonthlyStatistics+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIMonthlyStatistics+Map.swift new file mode 100644 index 0000000..4146bcf --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIMonthlyStatistics+Map.swift @@ -0,0 +1,71 @@ +// +// APIMonthlyStatistics+Map.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension APIMonthlyStatistics { + var monthlyPrecipitationStatistics: MonthlyWeatherStatistics { + MonthlyWeatherStatistics( + months: months.map(\.precipitationStatistics), + baselineStartDate: Date.daysFromEpoch(baselineStart), + metadata: metadata.weatherMetadata + ) + } + + var monthlyTemperatureStatistics: MonthlyWeatherStatistics { + MonthlyWeatherStatistics( + months: months.map(\.temperatureStatistics), + baselineStartDate: Date.daysFromEpoch(baselineStart), + metadata: metadata.weatherMetadata + ) + } + + @usableFromInline + func parse(query: MonthlyWeatherStatisticsQuery) -> MonthlyWeatherStatistics { + switch query.statisticsType { + case .temperature: + guard let stats = monthlyTemperatureStatistics as? MonthlyWeatherStatistics else { + preconditionFailure("Type mismatch: expected MonthlyWeatherStatistics but got MonthlyWeatherStatistics<\(T.self)>") + } + return stats + case .precipitation: + guard let stats = monthlyPrecipitationStatistics as? MonthlyWeatherStatistics else { + preconditionFailure("Type mismatch: expected MonthlyWeatherStatistics but got MonthlyWeatherStatistics<\(T.self)>") + } + return stats + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension APIMonthlyStatisticsData { + var precipitationStatistics: MonthPrecipitationStatistics { + guard let precipitation else { + preconditionFailure("Precipitation is nil, this could be a logic error") + } + + return MonthPrecipitationStatistics( + month: month, + averagePrecipitationProbability: Double(precipitation.probability), + averagePrecipitationAmount: Measurement(value: precipitation.averageAmount, unit: .millimeters), + averageSnowfallAmount: Measurement(value: precipitation.averageSnowfallAmount, unit: .millimeters) + ) + } + + var temperatureStatistics: MonthTemperatureStatistics { + guard let temperature else { + preconditionFailure("Temperature is nil, this could be a logic error") + } + + return MonthTemperatureStatistics( + month: month, + averageLowTemperature: Measurement(value: temperature.min ?? .nan, unit: .celsius), + averageHighTemperature: Measurement(value: temperature.max ?? .nan, unit: .celsius) + ) + } +} diff --git a/Sources/OpenWeatherKit/Internal/Extensions/Date+Utils.swift b/Sources/OpenWeatherKit/Internal/Extensions/Date+Utils.swift index 5a679cf..52a053b 100644 --- a/Sources/OpenWeatherKit/Internal/Extensions/Date+Utils.swift +++ b/Sources/OpenWeatherKit/Internal/Extensions/Date+Utils.swift @@ -19,4 +19,76 @@ extension Date { components.hour = hours return Calendar.current.date(byAdding: components, to: Date())! } + + static func daysFromEpoch(_ days: Int) -> Date { + // Create a date from the Unix epoch (1970-01-01) + let epoch = Date(timeIntervalSince1970: 0) + + // Convert days to TimeInterval (seconds) and subtract 1 day since 1970-01-01 is day 1 + let timeInterval = TimeInterval((days - 1) * 24 * 60 * 60) + return Date(timeInterval: timeInterval, since: epoch) + } + + /// Computes start and end dates for a day-of-year range, handling wrap-around scenarios. + /// + /// - Parameters: + /// - startDay: The first day of the span, between 1 and 366. + /// - endDay: The last day of the span, between 1 and 366. + /// - calendar: The calendar to use for date calculations (should be UTC). + /// - referenceYear: The reference year to use for date calculations. + /// - Throws: `WeatherError.invalidRequest` if dates cannot be computed. + /// - Returns: A tuple containing the start and end dates for the range. + /// + /// For normal cases where `endDay >= startDay`, both dates use the reference year. + /// For wrap-around cases where `startDay > endDay`, the start date uses `referenceYear - 1` + /// and the end date uses `referenceYear`, allowing for ranges like "Dec 31 to Jan 2". + /// + /// Example: `startDay: 365, endDay: 2` with `referenceYear: 2024` returns: + /// - Start: December 31, 2023 + /// - End: January 2, 2024 + @usableFromInline + static func computeDateRange( + startDay: Int, + endDay: Int, + calendar: Calendar, + referenceYear: Int + ) throws -> (startDate: Date, endDate: Date) { + guard (1...366).contains(startDay), (1...366).contains(endDay) else { + throw WeatherError.invalidRequest("startDay and endDay must be within 1...366") + } + + // Helper to compute a concrete Date from a day-of-year and year + func dateFor(dayOfYear: Int, year: Int) throws -> Date { + guard let startOfYear = calendar.date(from: DateComponents(year: year, month: 1, day: 1)) else { + throw WeatherError.invalidRequest("Unable to create start of year date for year \(year)") + } + + // Check if the requested day exists in the given year + let daysInYear = calendar.range(of: .day, in: .year, for: startOfYear)?.count ?? 365 + guard dayOfYear <= daysInYear else { + throw WeatherError.invalidRequest("Day \(dayOfYear) does not exist in year \(year)") + } + + guard let result = calendar.date(byAdding: .day, value: dayOfYear - 1, to: startOfYear) else { + throw WeatherError.invalidRequest("Unable to create date for day \(dayOfYear) in year \(year)") + } + return result + } + + let startDate: Date + let endDate: Date + + if endDay >= startDay { + // Normal case: same year + startDate = try dateFor(dayOfYear: startDay, year: referenceYear) + endDate = try dateFor(dayOfYear: endDay, year: referenceYear) + } else { + // Wrap-around case: for historical data, this means spanning across year boundary + // e.g., startDay: 365, endDay: 2 means Dec 31 (previous year) to Jan 2 (current year) + startDate = try dateFor(dayOfYear: startDay, year: referenceYear - 1) + endDate = try dateFor(dayOfYear: endDay, year: referenceYear) + } + + return (startDate: startDate, endDate: endDate) + } } diff --git a/Sources/OpenWeatherKit/Internal/Extensions/DateFormatter+Utils.swift b/Sources/OpenWeatherKit/Internal/Extensions/DateFormatter+Utils.swift new file mode 100644 index 0000000..ce04c5c --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Extensions/DateFormatter+Utils.swift @@ -0,0 +1,30 @@ +import Foundation + +extension DateFormatter { + /// A DateFormatter configured for API date strings in YYYY-MM-DD format. + /// Uses UTC timezone to avoid DST edge cases. + @usableFromInline + static let dateOnly: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + formatter.timeZone = TimeZone(identifier: "UTC") + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + }() +} + +extension Date { + /// Converts a Date to a YYYY-MM-DD string in UTC. + @usableFromInline + func toDateString() -> String { + DateFormatter.dateOnly.string(from: self) + } + + /// Creates a Date from a YYYY-MM-DD string in UTC. + /// - Parameter dateString: String in YYYY-MM-DD format + /// - Returns: Date object or nil if parsing fails + @usableFromInline + static func fromDateString(_ dateString: String) -> Date? { + DateFormatter.dateOnly.date(from: dateString) + } +} diff --git a/Sources/OpenWeatherKit/Internal/Extensions/Sendable+Compat.swift b/Sources/OpenWeatherKit/Internal/Extensions/Sendable+Compat.swift deleted file mode 100644 index 934c705..0000000 --- a/Sources/OpenWeatherKit/Internal/Extensions/Sendable+Compat.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Sendable+Compat.swift -// -// -// Created by Jeremy Greenwood on 11/24/22. -// - -import Foundation - -extension Date: @unchecked Sendable {} -extension Measurement: @unchecked Sendable {} -extension URL: @unchecked Sendable {} -extension UnitAngle: @unchecked Sendable {} -extension UnitLength: @unchecked Sendable {} -extension UnitPressure: @unchecked Sendable {} -extension UnitSpeed: @unchecked Sendable {} -extension UnitTemperature: @unchecked Sendable {} - -#if !os(Linux) -extension URLSession: @unchecked Sendable {} -#endif diff --git a/Sources/OpenWeatherKit/Internal/Models/APIDailySummary.swift b/Sources/OpenWeatherKit/Internal/Models/APIDailySummary.swift new file mode 100644 index 0000000..1267f9c --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Models/APIDailySummary.swift @@ -0,0 +1,42 @@ +// +// APIDailySummary.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +// MARK: - APIDailySummary +@usableFromInline +struct APIDailySummary: Codable, Sendable, Equatable { + let metadata: APIMetadata + let days: [APIDailySummaryDay] + let startDate: Int // Days from epoch + let endDate: Int // Days from epoch + + enum CodingKeys: String, CodingKey { + case metadata + case days + case startDate + case endDate + } +} + +// MARK: - APIDailySummaryDay +@usableFromInline +struct APIDailySummaryDay: Codable, Sendable, Equatable { + let date: Int // Days from epoch (e.g., days since 1970-01-01) + let temperatureMin: Double + let temperatureMax: Double + let precipitationAmount: Double + let snowfallAmount: Double + + enum CodingKeys: String, CodingKey { + case date + case temperatureMin + case temperatureMax + case precipitationAmount + case snowfallAmount + } +} diff --git a/Sources/OpenWeatherKit/Internal/Models/APIStatistics.swift b/Sources/OpenWeatherKit/Internal/Models/APIStatistics.swift new file mode 100644 index 0000000..7103b02 --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Models/APIStatistics.swift @@ -0,0 +1,122 @@ +// +// APIStatistics.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +// MARK: - APIHourlyStatistics +@usableFromInline +struct APIHourlyStatistics: Codable, Sendable, Equatable { + let metadata: APIMetadata + let hours: [APIHourlyStatisticsData] + let baselineStart: Int + + enum CodingKeys: String, CodingKey { + case metadata + case hours + case baselineStart + } +} + +// MARK: - APIHourlyStatisticsData +@usableFromInline +struct APIHourlyStatisticsData: Codable, Sendable, Equatable { + let hourOfYear: Int // Hour of year (1-8784) + let temperature: APITemperatureStatistics + + enum CodingKeys: String, CodingKey { + case hourOfYear + case temperature + } +} + +// MARK: - APIDailyStatistics +@usableFromInline +struct APIDailyStatistics: Codable, Sendable, Equatable { + let metadata: APIMetadata + let days: [APIDailyStatisticsData] + let baselineStart: Int + + enum CodingKeys: String, CodingKey { + case metadata + case days + case baselineStart + } +} + +// MARK: - APIDailyStatisticsData +@usableFromInline +struct APIDailyStatisticsData: Codable, Sendable, Equatable { + let dayOfYear: Int // Day of year (1-366) + let temperature: APITemperatureStatistics? + let precipitation: APIPrecipitationStatistics? + + enum CodingKeys: String, CodingKey { + case dayOfYear + case temperature + case precipitation + } +} + +// MARK: - APIMonthlyStatistics +@usableFromInline +struct APIMonthlyStatistics: Codable, Sendable, Equatable { + let metadata: APIMetadata + let months: [APIMonthlyStatisticsData] + let baselineStart: Int + + enum CodingKeys: String, CodingKey { + case metadata + case months + case baselineStart + } +} + +// MARK: - APIMonthlyStatisticsData +@usableFromInline +struct APIMonthlyStatisticsData: Codable, Sendable, Equatable { + let month: Int // Month (1-12) + let temperature: APITemperatureStatistics? + let precipitation: APIPrecipitationStatistics? + + enum CodingKeys: String, CodingKey { + case month + case temperature + case precipitation + } +} + +// MARK: - APITemperatureStatistics +@usableFromInline +struct APITemperatureStatistics: Codable, Sendable, Equatable { + let min: Double? + let max: Double? + let p10: Double? // 10th percentile + let p50: Double? // 50th percentile (median) + let p90: Double? // 90th percentile + + enum CodingKeys: String, CodingKey { + case min + case max + case p10 + case p50 + case p90 + } +} + +// MARK: - APIPrecipitationStatistics +@usableFromInline +struct APIPrecipitationStatistics: Codable, Sendable, Equatable { + let probability: Int + let averageAmount: Double + let averageSnowfallAmount: Double + + enum CodingKeys: String, CodingKey { + case probability + case averageAmount + case averageSnowfallAmount + } +} diff --git a/Sources/OpenWeatherKit/Internal/NetworkClient.swift b/Sources/OpenWeatherKit/Internal/NetworkClient.swift index c28be45..ab43f62 100644 --- a/Sources/OpenWeatherKit/Internal/NetworkClient.swift +++ b/Sources/OpenWeatherKit/Internal/NetworkClient.swift @@ -34,6 +34,106 @@ struct NetworkClient: Sendable { ) } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @usableFromInline + func fetchDailySummary( + location: LocationProtocol, + dataSets: repeat each Q, + startDate: Date, + endDate: Date, + jwt: String + ) async throws -> APIDailySummary { + var names: [String] = [] + repeat names.append((each dataSets).statisticsType.dataSet) + + let queryItems = [ + URLQueryItem(name: "dataSets", value: names.joined(separator: ",")), + URLQueryItem(name: "start", value: startDate.toDateString()), + URLQueryItem(name: "end", value: endDate.toDateString()) + ] + + return try await get( + .dailySummary(location), + queryItems: queryItems, + jwt: jwt + ) + } + + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @usableFromInline + func fetchHourlyStatistics( + location: LocationProtocol, + dataSets: repeat each Q, + startHour: Int, + endHour: Int, + jwt: String + ) async throws -> APIHourlyStatistics { + var names: [String] = [] + repeat names.append((each dataSets).statisticsType.dataSet) + + let queryItems = [ + URLQueryItem(name: "dataSets", value: names.joined(separator: ",")), + URLQueryItem(name: "startHour", value: "\(startHour)"), + URLQueryItem(name: "endHour", value: "\(endHour)") + ] + + return try await get( + .statistics(.hourly, location), + queryItems: queryItems, + jwt: jwt + ) + } + + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @usableFromInline + func fetchDailyStatistics( + location: LocationProtocol, + dataSets: repeat each Q, + startDay: Int, + endDay: Int, + jwt: String + ) async throws -> APIDailyStatistics { + var names: [String] = [] + repeat names.append((each dataSets).statisticsType.dataSet) + + let queryItems = [ + URLQueryItem(name: "dataSets", value: names.joined(separator: ",")), + URLQueryItem(name: "startDay", value: "\(startDay)"), + URLQueryItem(name: "endDay", value: "\(endDay)") + ] + + return try await get( + .statistics(.daily, location), + queryItems: queryItems, + jwt: jwt + ) + } + + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @usableFromInline + func fetchMonthlyStatistics( + location: LocationProtocol, + dataSets: repeat each Q, + startMonth: Int, + endMonth: Int, + jwt: String + ) async throws -> APIMonthlyStatistics { + var names: [String] = [] + repeat names.append((each dataSets).statisticsType.dataSet) + + let queryItems = [ + URLQueryItem(name: "dataSets", value: names.joined(separator: ",")), + URLQueryItem(name: "startMonth", value: "\(startMonth)"), + URLQueryItem(name: "endMonth", value: "\(endMonth)") + ] + + return try await get( + .statistics(.monthly, location), + queryItems: queryItems, + jwt: jwt + ) + } + @usableFromInline func fetchWeather( location: LocationProtocol, diff --git a/Sources/OpenWeatherKit/Internal/Route.swift b/Sources/OpenWeatherKit/Internal/Route.swift index 06a3ac1..359fdd4 100644 --- a/Sources/OpenWeatherKit/Internal/Route.swift +++ b/Sources/OpenWeatherKit/Internal/Route.swift @@ -7,17 +7,31 @@ import Foundation +enum StatisticsGranularity: String { + case hourly + case daily + case monthly +} + enum Route { case availability(LocationProtocol) case weather(WeatherService.Configuration.Language, LocationProtocol) + case dailySummary(LocationProtocol) + case statistics(StatisticsGranularity, LocationProtocol) var url: URL { let urlString: String = { let base = "https://weatherkit.apple.com" switch self { - case let .availability(location): return "\(base)/api/v2/availability/\(location.latitude)/\(location.longitude)" - case let .weather(language, location): return "\(base)/api/v2/weather/\(language.rawValue)/\(location.latitude)/\(location.longitude)" + case let .availability(location): + return "\(base)/api/v2/availability/\(location.latitude)/\(location.longitude)" + case let .weather(language, location): + return "\(base)/api/v2/weather/\(language.rawValue)/\(location.latitude)/\(location.longitude)" + case let .dailySummary(location): + return "\(base)/api/v2/summary/daily/\(location.latitude)/\(location.longitude)" + case let .statistics(granularity, location): + return "\(base)/api/v2/statistics/\(granularity.rawValue)/\(location.latitude)/\(location.longitude)" } }() diff --git a/Sources/OpenWeatherKit/Internal/StatisticsQuery.swift b/Sources/OpenWeatherKit/Internal/StatisticsQuery.swift new file mode 100644 index 0000000..7b67b79 --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/StatisticsQuery.swift @@ -0,0 +1,40 @@ +// +// StatisticsQuery.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/30/25. +// + +import Foundation + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@usableFromInline +protocol StatisticsQuery { + var statisticsType: StatisticsType { get } +} + +@usableFromInline +enum StatisticsType: Sendable { + case precipitation + case temperature + + @usableFromInline + var dataSet: String { + switch self { + case .precipitation: "precipitation" + case .temperature: "temperature" + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension DailyWeatherStatisticsQuery: StatisticsQuery {} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension HourlyWeatherStatisticsQuery: StatisticsQuery {} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MonthlyWeatherStatisticsQuery: StatisticsQuery {} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension DailyWeatherSummaryQuery: StatisticsQuery {} diff --git a/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift b/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift index 609ab20..eb3c24c 100644 --- a/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift +++ b/Sources/OpenWeatherKit/Public/Protocols/LocationProtocol.swift @@ -9,7 +9,7 @@ import Foundation #if canImport(CoreLocation) import CoreLocation -extension CLLocation: LocationProtocol, @unchecked Sendable { +extension CLLocation: LocationProtocol { public var latitude: Double { coordinate.latitude } public var longitude: Double { coordinate.longitude } } diff --git a/Sources/OpenWeatherKit/Public/Requests/DailyWeatherStatisticsQuery.swift b/Sources/OpenWeatherKit/Public/Requests/DailyWeatherStatisticsQuery.swift new file mode 100644 index 0000000..89467d3 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Requests/DailyWeatherStatisticsQuery.swift @@ -0,0 +1,30 @@ +// +// DailyWeatherStatisticsQuery.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// A structure that encapsulates a daily weather statistics dataset request. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct DailyWeatherStatisticsQuery: Sendable where T: Decodable, T: Encodable, T: Equatable, T: Sendable { + @usableFromInline + internal let statisticsType: StatisticsType + + @usableFromInline + internal init(statisticsType: StatisticsType) { + self.statisticsType = statisticsType + } + + /// The daily temperature statistics query. + public static var temperature: DailyWeatherStatisticsQuery { + DailyWeatherStatisticsQuery(statisticsType: .temperature) + } + + /// The daily precipitation statistics query. + public static var precipitation: DailyWeatherStatisticsQuery { + DailyWeatherStatisticsQuery(statisticsType: .precipitation) + } +} diff --git a/Sources/OpenWeatherKit/Public/Requests/DailyWeatherSummaryQuery.swift b/Sources/OpenWeatherKit/Public/Requests/DailyWeatherSummaryQuery.swift new file mode 100644 index 0000000..a650c1a --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Requests/DailyWeatherSummaryQuery.swift @@ -0,0 +1,30 @@ +// +// DailyWeatherSummaryQuery.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// A structure that encapsulates a daily weather summary dataset request. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct DailyWeatherSummaryQuery: Sendable where T: Decodable, T: Encodable, T: Equatable, T: Sendable { + @usableFromInline + internal let statisticsType: StatisticsType + + @usableFromInline + internal init(statisticsType: StatisticsType) { + self.statisticsType = statisticsType + } + + /// The temperature summary query. + public static var temperature: DailyWeatherSummaryQuery { + DailyWeatherSummaryQuery(statisticsType: .temperature) + } + + /// The precipitation summary query. + public static var precipitation: DailyWeatherSummaryQuery { + DailyWeatherSummaryQuery(statisticsType: .precipitation) + } +} diff --git a/Sources/OpenWeatherKit/Public/Requests/HourlyWeatherStatisticsQuery.swift b/Sources/OpenWeatherKit/Public/Requests/HourlyWeatherStatisticsQuery.swift new file mode 100644 index 0000000..ed1bf28 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Requests/HourlyWeatherStatisticsQuery.swift @@ -0,0 +1,25 @@ +// +// HourlyWeatherStatisticsQuery.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// A structure that encapsulates an hourly weather statistics dataset request. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct HourlyWeatherStatisticsQuery: Sendable where T: Decodable, T: Encodable, T: Equatable, T: Sendable { + @usableFromInline + internal let statisticsType: StatisticsType + + @usableFromInline + internal init(statisticsType: StatisticsType) { + self.statisticsType = statisticsType + } + + /// The hourly temperature statistics query. + public static var temperature: HourlyWeatherStatisticsQuery { + HourlyWeatherStatisticsQuery(statisticsType: .temperature) + } +} diff --git a/Sources/OpenWeatherKit/Public/Requests/MonthlyWeatherStatisticsQuery.swift b/Sources/OpenWeatherKit/Public/Requests/MonthlyWeatherStatisticsQuery.swift new file mode 100644 index 0000000..e6ae4b1 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Requests/MonthlyWeatherStatisticsQuery.swift @@ -0,0 +1,30 @@ +// +// MonthlyWeatherStatisticsQuery.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// A structure that encapsulates a monthly weather statistics dataset request. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct MonthlyWeatherStatisticsQuery: Sendable where T: Decodable, T: Encodable, T: Equatable, T: Sendable { + @usableFromInline + internal let statisticsType: StatisticsType + + @usableFromInline + internal init(statisticsType: StatisticsType) { + self.statisticsType = statisticsType + } + + /// The monthly temperature statistics query. + public static var temperature: MonthlyWeatherStatisticsQuery { + MonthlyWeatherStatisticsQuery(statisticsType: .temperature) + } + + /// The monthly precipitation statistics query. + public static var precipitation: MonthlyWeatherStatisticsQuery { + MonthlyWeatherStatisticsQuery(statisticsType: .precipitation) + } +} diff --git a/Sources/OpenWeatherKit/Public/Statistics/DailyWeatherStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/DailyWeatherStatistics.swift new file mode 100644 index 0000000..7e8b96d --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Statistics/DailyWeatherStatistics.swift @@ -0,0 +1,41 @@ +// +// DailyWeatherStatistics.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// A structure that contains daily climatological statistics for a location. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct DailyWeatherStatistics: Codable, Equatable, Sendable, RandomAccessCollection where T: Decodable, T: Encodable, T: Equatable, T: Sendable { + + /// A type representing the sequence's elements. + public typealias Element = T + + /// A type that represents a position in the collection. + /// + /// Valid indices consist of the position of every element and a + /// "past the end" position that's not valid for use as a subscript + /// argument. + public typealias Index = Int + + /// An ordered collection of day weather statistics data of type `T`, for each requested day. + public var days: [T] + + /// The year the statistics collection began. + public var baselineStartDate: Date + + /// Descriptive information about the weather statistics data. + public var metadata: WeatherMetadata + + /// The start index for the hourly weather statistics. + public var startIndex: DailyWeatherStatistics.Index { days.startIndex } + + /// The end index for the hourly weather statistics. + public var endIndex: DailyWeatherStatistics.Index { days.endIndex } + + /// The hour weather statistics at the provided index. + public subscript(position: DailyWeatherStatistics.Index) -> DailyWeatherStatistics.Element { days[position] } +} diff --git a/Sources/OpenWeatherKit/Public/Statistics/DayPrecipitationStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/DayPrecipitationStatistics.swift new file mode 100644 index 0000000..2afb134 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Statistics/DayPrecipitationStatistics.swift @@ -0,0 +1,26 @@ +// +// DayPrecipitationStatistics.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// Precipitation statistics for a specific day of the year. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct DayPrecipitationStatistics: Codable, Equatable, Sendable { + /// The day of the year, in UTC. + /// + /// The day ranges from 1 to 366. + public var day: Int + + /// The average percentage probability of precipitation (0.0 = 0%, 1.0 = 100%) for the day. + public var averagePrecipitationProbability: Double + + /// The average amount of liquid precipitation for the day. + public var averagePrecipitationAmount: Measurement + + /// The average amount of snowfall as depth of snow crystals for the day. + public var averageSnowfallAmount: Measurement +} diff --git a/Sources/OpenWeatherKit/Public/Statistics/DayTemperatureStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/DayTemperatureStatistics.swift new file mode 100644 index 0000000..1e9326d --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Statistics/DayTemperatureStatistics.swift @@ -0,0 +1,33 @@ +// +// DayTemperatureStatistics.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// Temperature statistics for a specific day of the year. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct DayTemperatureStatistics: Codable, Equatable, Sendable { + /// The day of the year, in UTC. + /// + /// The day ranges from 1 to 366. + public var day: Int + + /// The average observed low temperature for the day. + public var averageLowTemperature: Measurement + + /// The average observed high temperature for the day. + public var averageHighTemperature: Measurement + + public init( + day: Int, + averageLowTemperature: Measurement, + averageHighTemperature: Measurement + ) { + self.day = day + self.averageLowTemperature = averageLowTemperature + self.averageHighTemperature = averageHighTemperature + } +} diff --git a/Sources/OpenWeatherKit/Public/Statistics/HourTemperatureStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/HourTemperatureStatistics.swift new file mode 100644 index 0000000..74391d5 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Statistics/HourTemperatureStatistics.swift @@ -0,0 +1,20 @@ +// +// HourTemperatureStatistics.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// Temperature statistics for a specific hour of the year. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct HourTemperatureStatistics: Codable, Equatable, Sendable { + /// The hour of the year, in UTC. + /// + /// The hour ranges from 1 to 8784. + public var hour: Int + + /// The temperature statistics for the hour. + public var percentiles: Percentiles +} diff --git a/Sources/OpenWeatherKit/Public/Statistics/HourlyWeatherStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/HourlyWeatherStatistics.swift new file mode 100644 index 0000000..03b63c5 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Statistics/HourlyWeatherStatistics.swift @@ -0,0 +1,41 @@ +// +// HourlyWeatherStatistics.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// A structure that contains hourly climatological statistics for a location. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct HourlyWeatherStatistics: Codable, Equatable, Sendable, RandomAccessCollection where T: Decodable, T: Encodable, T: Equatable, T: Sendable { + + /// A type representing the sequence's elements. + public typealias Element = T + + /// A type that represents a position in the collection. + /// + /// Valid indices consist of the position of every element and a + /// "past the end" position that's not valid for use as a subscript + /// argument. + public typealias Index = Int + + /// An ordered collection of hour weather statistics data of type `T`, for each requested hour. + public var hours: [T] + + /// The date when the statistics collection began. + public var baselineStartDate: Date + + /// Descriptive information about the weather statistics data. + public var metadata: WeatherMetadata + + /// The start index for the hourly weather statistics. + public var startIndex: HourlyWeatherStatistics.Index { hours.startIndex } + + /// The end index for the hourly weather statistics. + public var endIndex: HourlyWeatherStatistics.Index { hours.endIndex } + + /// The hour weather statistics at the provided index. + public subscript(position: HourlyWeatherStatistics.Index) -> HourlyWeatherStatistics.Element { hours[position] } +} diff --git a/Sources/OpenWeatherKit/Public/Statistics/MonthPrecipitationStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/MonthPrecipitationStatistics.swift new file mode 100644 index 0000000..a90810f --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Statistics/MonthPrecipitationStatistics.swift @@ -0,0 +1,26 @@ +// +// MonthPrecipitationStatistics.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// Precipitation statistics for a specific month. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct MonthPrecipitationStatistics: Codable, Equatable, Sendable { + /// The month of the year, in UTC. + /// + /// The Gregorian month of the year ranges from 1 (January) to 12 (December). + public var month: Int + + /// The average percentage probability of precipitation (0.0 = 0%, 1.0 = 100%) for the month. + public var averagePrecipitationProbability: Double + + /// The average amount of liquid precipitation for the month. + public var averagePrecipitationAmount: Measurement + + /// The average amount of snowfall as depth of snow crystals for the month. + public var averageSnowfallAmount: Measurement +} diff --git a/Sources/OpenWeatherKit/Public/Statistics/MonthTemperatureStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/MonthTemperatureStatistics.swift new file mode 100644 index 0000000..75bc6fa --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Statistics/MonthTemperatureStatistics.swift @@ -0,0 +1,24 @@ +// +// MonthTemperatureStatistics.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// Temperature statistics for a specific month. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct MonthTemperatureStatistics: Codable, Equatable, Sendable { + + /// The month of the year, in UTC. + /// + /// The Gregorian month of the year ranges from 1 (January) to 12 (December). + public var month: Int + + /// The average observed low temperature for the month. + public var averageLowTemperature: Measurement + + /// The average observed high temperature for the month. + public var averageHighTemperature: Measurement +} diff --git a/Sources/OpenWeatherKit/Public/Statistics/MonthlyWeatherStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/MonthlyWeatherStatistics.swift new file mode 100644 index 0000000..02974a1 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Statistics/MonthlyWeatherStatistics.swift @@ -0,0 +1,41 @@ +// +// MonthlyWeatherStatistics.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// A structure that contains monthly climatological statistics for a location. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct MonthlyWeatherStatistics: Codable, Equatable, Sendable, RandomAccessCollection where T: Decodable, T: Encodable, T: Equatable, T: Sendable { + + /// A type representing the sequence's elements. + public typealias Element = T + + /// A type that represents a position in the collection. + /// + /// Valid indices consist of the position of every element and a + /// "past the end" position that's not valid for use as a subscript + /// argument. + public typealias Index = Int + + /// An ordered collection of month weather statistics data of type `T`, for each requested month. + public var months: [T] + + /// The year the statistics collection began. + public var baselineStartDate: Date + + /// Descriptive information about the weather statistics data. + public var metadata: WeatherMetadata + + /// The start index for the monthly weather statistics. + public var startIndex: MonthlyWeatherStatistics.Index { months.startIndex } + + /// The end index for the monthly weather statistics. + public var endIndex: MonthlyWeatherStatistics.Index { months.endIndex } + + /// The month weather statistics at the provided index. + public subscript(position: MonthlyWeatherStatistics.Index) -> MonthlyWeatherStatistics.Element { months[position] } +} diff --git a/Sources/OpenWeatherKit/Public/Statistics/Percentiles.swift b/Sources/OpenWeatherKit/Public/Statistics/Percentiles.swift new file mode 100644 index 0000000..a322e98 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Statistics/Percentiles.swift @@ -0,0 +1,24 @@ +// +// Percentiles.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// +/// A structure that describes probability distributions for a measurable weather condition. +/// +@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +public struct Percentiles : Codable, Equatable, Sendable where Dimension : Unit { + + /// 10% of the distribution is less than this value. + public var p10: Measurement + + /// 50% of the distribution is less than this value. + public var p50: Measurement + + /// 90% of the distribution is less than this value. + public var p90: Measurement +} diff --git a/Sources/OpenWeatherKit/Public/Summary/DailyWeatherSummary.swift b/Sources/OpenWeatherKit/Public/Summary/DailyWeatherSummary.swift new file mode 100644 index 0000000..cec80f6 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Summary/DailyWeatherSummary.swift @@ -0,0 +1,40 @@ +// +// DailyWeatherSummary.swift +// +// +// Created by Jeremy Greenwood on 10/29/25. +// + +import Foundation + +/// +/// A structure that holds a collection of day weather summaries. +/// +@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +public struct DailyWeatherSummary : Codable, Equatable, Sendable, RandomAccessCollection where T : Decodable, T : Encodable, T : Equatable, T : Sendable { + + /// A type representing the sequence's elements. + public typealias Element = T + + /// A type that represents a position in the collection. + /// + /// Valid indices consist of the position of every element and a + /// "past the end" position that's not valid for use as a subscript + /// argument. + public typealias Index = Int + + /// An ordered collection of day weather summaries of type `T`, for each requested day. + public var days: [T] + + /// Descriptive information about the weather statistics data. + public var metadata: WeatherMetadata + + /// The start index for the daily weather summaries. + public var startIndex: DailyWeatherSummary.Index { days.startIndex } + + /// The end index for the daily weather summaries. + public var endIndex: DailyWeatherSummary.Index { days.endIndex } + + /// The day weather summary at the provided index. + public subscript(position: DailyWeatherSummary.Index) -> DailyWeatherSummary.Element { days[position] } +} diff --git a/Sources/OpenWeatherKit/Public/Summary/DayPrecipitationSummary.swift b/Sources/OpenWeatherKit/Public/Summary/DayPrecipitationSummary.swift new file mode 100644 index 0000000..7301e0f --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Summary/DayPrecipitationSummary.swift @@ -0,0 +1,24 @@ +// +// DayPrecipitationSummary.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/30/25. +// + +import Foundation + +/// +/// A structure that describes the precipitation summary for a day. +/// +@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +public struct DayPrecipitationSummary : Codable, Equatable, Sendable { + + /// The day of the observed precipitation summary + public var date: Date + + /// The amount of liquid precipitation for the day. + public var precipitationAmount: Measurement + + /// The snowfall amount as depth of snow crystals for the day. + public var snowfallAmount: Measurement +} diff --git a/Sources/OpenWeatherKit/Public/Summary/DayTemperatureSummary.swift b/Sources/OpenWeatherKit/Public/Summary/DayTemperatureSummary.swift new file mode 100644 index 0000000..b9f70a0 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Summary/DayTemperatureSummary.swift @@ -0,0 +1,24 @@ +// +// DayTemperatureSummary.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/30/25. +// + +import Foundation + +/// +/// A structure that describes the temperature summary for a day. +/// +@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +public struct DayTemperatureSummary : Codable, Equatable, Sendable { + + /// The day of the observed temperature summary. + public var date: Date + + /// The observed low temperature for the day. + public var lowTemperature: Measurement + + /// The observed high temperature for the day. + public var highTemperature: Measurement +} diff --git a/Sources/OpenWeatherKit/Public/WeatherError.swift b/Sources/OpenWeatherKit/Public/WeatherError.swift index 9593b6e..8024e8e 100644 --- a/Sources/OpenWeatherKit/Public/WeatherError.swift +++ b/Sources/OpenWeatherKit/Public/WeatherError.swift @@ -24,6 +24,9 @@ public enum WeatherError : LocalizedError, Equatable, Hashable { case missingData(_ attributeName: String) + /// An invalid request parameter. + case invalidRequest(_ message: String) + /// A localized message describing what error occurred. public var errorDescription: String? { switch self { @@ -36,6 +39,8 @@ public enum WeatherError : LocalizedError, Equatable, Hashable { return NSLocalizedString("Error.timezone", bundle: Bundle.module, comment: "Could not determine timezone") case let .missingData(name): return String(format: NSLocalizedString("Error.missingData", bundle: Bundle.module, comment: "The data \(name) is missing from the response"), name) + case let .invalidRequest(message): + return message } } @@ -51,6 +56,8 @@ public enum WeatherError : LocalizedError, Equatable, Hashable { return NSLocalizedString("Error.timezone", bundle: Bundle.module, comment: "Could not determine timezone") case let .missingData(name): return String(format: NSLocalizedString("Error.missingData", bundle: Bundle.module, comment: "The data \(name) is missing from the response"), name) + case let .invalidRequest(message): + return message } } diff --git a/Sources/OpenWeatherKit/Public/WeatherService+Forecast.swift b/Sources/OpenWeatherKit/Public/WeatherService+Forecast.swift new file mode 100644 index 0000000..f8a9296 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/WeatherService+Forecast.swift @@ -0,0 +1,414 @@ +// +// WeatherService+Forecast.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/30/25. +// + +import Foundation +#if canImport(CoreLocation) +import CoreLocation +#endif + +extension WeatherService { +#if canImport(CoreLocation) + /// + /// Returns the weather forecast for the requested location. Includes all available weather data sets. + /// - Parameter location: The requested location. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The aggregate weather. + /// + @inlinable + final public func weather(for location: LocationProtocol) async throws -> Weather { + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await getWeather(location: location, countryCode: countryCode, timezone: timezone) + } +#endif + + /// + /// Returns the weather forecast for the requested location. Includes all available weather data sets. + /// - Parameter location: The requested location. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The aggregate weather. + /// + @inlinable + final public func weather( + for location: LocationProtocol, + countryCode: String, + timezone: TimeZone, + language: WeatherService.Configuration.Language? = nil + ) async throws -> Weather { + try await getWeather(location: location, countryCode: countryCode, timezone: timezone, language: language) + } + + /// + /// Returns the weather forecast for the requested location. + /// - Parameters: + /// - location: The requested location. + /// - including: Weather query + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested weather data set. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. + /// + /// Example usage: + /// `let current = try await service.weather(for: newYork, including: .current)` + /// +#if canImport(CoreLocation) + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet: WeatherQuery + ) async throws -> T { + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet, countryCode: countryCode, timezone: timezone) + } +#endif + + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> T { + let _dataSet = countryCode.map { dataSet.update(with: $0) } ?? dataSet + + let proxy = try await networkClient.fetchWeather( + location: location, + language: self.configuration.language, + queries: _dataSet, + timezone: timezone, + jwt: self.configuration.jwt() + ) + + return try _dataSet.result(proxy) + } + + /// + /// Returns the weather forecast for the requested location. + /// - Parameters: + /// - location: The requested location. + /// - including: Weather queries + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested weather data set. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. + /// + /// Example usage: + /// `let (current, minute) = try await service.weather(for: newYork, including: .current, .minute)` + /// +#if canImport(CoreLocation) + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery + ) async throws -> (T1, T2) { + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet1, dataSet2, countryCode: countryCode, timezone: timezone) + } +#endif + + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> (T1, T2) { + let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 + let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 + + let proxy = try await networkClient.fetchWeather( + location: location, + language: self.configuration.language, + queries: _dataSet1, _dataSet2, + timezone: timezone, + jwt: self.configuration.jwt() + ) + + return try ( + _dataSet1.result(proxy), + _dataSet2.result(proxy) + ) + } + + /// + /// Returns the weather forecast for the requested location. + /// - Parameters: + /// - location: The requested location. + /// - including: Weather queries + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested weather data set. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. + /// +#if canImport(CoreLocation) + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery + ) async throws -> (T1, T2, T3) { + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, countryCode: countryCode, timezone: timezone) + } +#endif + + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> (T1, T2, T3) { + let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 + let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 + let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 + + let proxy = try await networkClient.fetchWeather( + location: location, + language: self.configuration.language, + queries: _dataSet1, _dataSet2, _dataSet3, + timezone: timezone, + jwt: self.configuration.jwt() + ) + + return try ( + _dataSet1.result(proxy), + _dataSet2.result(proxy), + _dataSet3.result(proxy) + ) + } + + /// + /// Returns the weather forecast for the requested location. + /// - Parameters: + /// - location: The requested location. + /// - including: Weather queries + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested weather data set. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. + /// +#if canImport(CoreLocation) + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery, + _ dataSet4: WeatherQuery + ) async throws -> (T1, T2, T3, T4) { + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, countryCode: countryCode, timezone: timezone) + } +#endif + + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery, + _ dataSet4: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> (T1, T2, T3, T4) { + let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 + let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 + let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 + let _dataSet4 = countryCode.map { dataSet4.update(with: $0) } ?? dataSet4 + + let proxy = try await networkClient.fetchWeather( + location: location, + language: self.configuration.language, + queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, + timezone: timezone, + jwt: self.configuration.jwt() + ) + + return try ( + _dataSet1.result(proxy), + _dataSet2.result(proxy), + _dataSet3.result(proxy), + _dataSet4.result(proxy) + ) + } + + /// + /// Returns the weather forecast for the requested location. + /// - Parameters: + /// - location: The requested location. + /// - including: Weather queries + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested weather data set. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. + /// +#if canImport(CoreLocation) + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery, + _ dataSet4: WeatherQuery, + _ dataSet5: WeatherQuery + ) async throws -> (T1, T2, T3, T4, T5) { + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, dataSet5, countryCode: countryCode, timezone: timezone) + } +#endif + + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery, + _ dataSet4: WeatherQuery, + _ dataSet5: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> (T1, T2, T3, T4, T5) { + let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 + let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 + let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 + let _dataSet4 = countryCode.map { dataSet4.update(with: $0) } ?? dataSet4 + let _dataSet5 = countryCode.map { dataSet5.update(with: $0) } ?? dataSet5 + + let proxy = try await networkClient.fetchWeather( + location: location, + language: self.configuration.language, + queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, _dataSet5, + timezone: timezone, + jwt: self.configuration.jwt() + ) + + return try ( + _dataSet1.result(proxy), + _dataSet2.result(proxy), + _dataSet3.result(proxy), + _dataSet4.result(proxy), + _dataSet5.result(proxy) + ) + } + + /// + /// Returns the weather forecast for the requested location. + /// - Parameters: + /// - location: The requested location. + /// - including: Weather queries + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested weather data set. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. + /// +#if canImport(CoreLocation) + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery, + _ dataSet4: WeatherQuery, + _ dataSet5: WeatherQuery, + _ dataSet6: WeatherQuery + ) async throws -> (T1, T2, T3, T4, T5, T6) { + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, dataSet5, dataSet6, countryCode: countryCode, timezone: timezone) + } +#endif + + @inlinable + final public func weather( + for location: LocationProtocol, + including dataSet1: WeatherQuery, + _ dataSet2: WeatherQuery, + _ dataSet3: WeatherQuery, + _ dataSet4: WeatherQuery, + _ dataSet5: WeatherQuery, + _ dataSet6: WeatherQuery, + countryCode: String? = nil, + timezone: TimeZone + ) async throws -> (T1, T2, T3, T4, T5, T6) { + let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 + let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 + let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 + let _dataSet4 = countryCode.map { dataSet4.update(with: $0) } ?? dataSet4 + let _dataSet5 = countryCode.map { dataSet5.update(with: $0) } ?? dataSet5 + let _dataSet6 = countryCode.map { dataSet6.update(with: $0) } ?? dataSet6 + + let proxy = try await networkClient.fetchWeather( + location: location, + language: self.configuration.language, + queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, _dataSet5, _dataSet6, + timezone: timezone, + jwt: self.configuration.jwt() + ) + + return try ( + _dataSet1.result(proxy), + _dataSet2.result(proxy), + _dataSet3.result(proxy), + _dataSet4.result(proxy), + _dataSet5.result(proxy), + _dataSet6.result(proxy) + ) + } +} + +extension WeatherService { +#if canImport(CoreLocation) + @usableFromInline + func resolveCountryCodeAndTimezone(for location: LocationProtocol) async throws -> (countryCode: String, timezone: TimeZone) { + guard let countryCode = try await geocoder.countryCode(location) else { + throw WeatherError.countryCode + } + guard let timezoneIdentifier = try await geocoder.timezone(location), + let timezone = TimeZone(identifier: timezoneIdentifier) else { + throw WeatherError.timezone + } + return (countryCode, timezone) + } +#endif + + @usableFromInline + func getWeather(location: LocationProtocol, countryCode: String, timezone: TimeZone, language: WeatherService.Configuration.Language? = nil) async throws -> Weather { + let proxy = try await networkClient.fetchWeather( + location: location, + language: language ?? self.configuration.language, + queries: WeatherQuery.current, + WeatherQuery?>.minute, + WeatherQuery>.hourly, + WeatherQuery>.daily, + WeatherQuery<[WeatherAlert]?>.alerts(countryCode: countryCode), + WeatherQuery.availability(countryCode: countryCode), + timezone: timezone, + jwt: self.configuration.jwt() + ) + + return try Weather( + currentWeather: proxy.currentWeather.unwrap( + or: WeatherError.missingData(APIWeather.CodingKeys.currentWeather.rawValue) + ), + minuteForecast: proxy.minuteForecast, + hourlyForecast: proxy.hourlyForecast.unwrap( + or: WeatherError.missingData(APIWeather.CodingKeys.forecastHourly.rawValue) + ), + dailyForecast: proxy.dailyForecast.unwrap( + or: WeatherError.missingData(APIWeather.CodingKeys.forecastDaily.rawValue) + ), + weatherAlerts: proxy.weatherAlerts, + availability: proxy.availability.unwrap( + or: WeatherError.missingData(QueryContants.availability) + ) + ) + } +} diff --git a/Sources/OpenWeatherKit/Public/WeatherService+Statistics.swift b/Sources/OpenWeatherKit/Public/WeatherService+Statistics.swift new file mode 100644 index 0000000..4e22f18 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/WeatherService+Statistics.swift @@ -0,0 +1,401 @@ +// +// WeatherService+Statistics.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/30/25. +// + +import Foundation + +// MARK: - Statistics +extension WeatherService { + /// + /// Returns daily weather statistics for the requested location, for each day from the start day to the end day, inclusively. + /// + /// - Parameters: + /// - location: The requested location. + /// - startDay: The first day of the span, between 1 and 366. + /// - endDay: The last day of the span, between 1 and 366. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested daily weather statistics + /// + /// The statistics returned for each day are derived from weather data recorded over the past decades, to the present date. Each item returned represents statistics for a particular day of the year, in UTC. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. The following example will return statistics for the first 10 days of the year. + /// + /// ``` + /// let (dailyPrecipitationStatistics, dailyTemperatureStatistics) = try await service.dailyStatistics(for: newYork, startDay: 1, endDay: 10, including: .precipitation, .temperature) + /// ``` + /// + /// If `startDay` is greater than `endDay`, then a wrap around will occur. This next example will return statistics for days 365, 366, 1, and 2. + /// + /// ```swift + /// let (dailyPrecipitationStatistics, dailyTemperatureStatistics) = try await service.dailyStatistics(for: newYork, startDay: 365, endDay: 2, including: .precipitation, .temperature) + /// ``` + /// + /// - Precondition: `startDay in 1...366 && endDay in 1...366` + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func dailyStatistics( + for location: LocationProtocol, + startDay: Int, + endDay: Int, + including: repeat DailyWeatherStatisticsQuery + ) async throws -> (repeat DailyWeatherStatistics) { + // Use a UTC Gregorian calendar to interpret day-of-year in UTC + var calendar = Calendar(identifier: .gregorian) + guard let utcTimeZone = TimeZone(secondsFromGMT: 0) else { + throw WeatherError.invalidRequest("Unable to create UTC timezone") + } + calendar.timeZone = utcTimeZone + + let now = Date() + let currentYear = calendar.component(.year, from: now) + + // Compute the date range + let (startDate, endDate) = try Date.computeDateRange( + startDay: startDay, + endDay: endDay, + calendar: calendar, + referenceYear: currentYear + ) + + // Build interval and delegate to date-interval overload; header docs note end is capped at 1 year after start + let interval = DateInterval(start: startDate, end: endDate) + return try await dailyStatistics( + for: location, + forDaysIn: interval, + including: repeat each including + ) + } + + /// + /// Returns daily weather statistics for the requested location, for each day within the specified date interval. + /// + /// - Parameters: + /// - location: The requested location. + /// - interval: The date interval for which to obtain daily weather statistics. The end date of the interval will be capped at 1 year after the start date. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested daily weather statistics. + /// + /// The statistics returned for each day are derived from weather data recorded over the past decades, to the present date. Each item returned represents statistics for a particular day of the year, in UTC. For example, if December 31, UTC time, is within the span, the statistics returned for that particular day will be taken from data recorded over the years for day 365 of the year, or 366 if December 31 of the span falls on a leap year. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. Here's an example: + /// + /// ``` + /// let (dailyPrecipitationStatistics, dailyTemperatureStatistics) = try await service.dailyStatistics(for: newYork, forDaysIn: timeInterval, including: .precipitation, .temperature) + /// ``` + /// + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func dailyStatistics( + for location: LocationProtocol, + forDaysIn interval: DateInterval, + including: repeat DailyWeatherStatisticsQuery + ) async throws -> (repeat DailyWeatherStatistics) { + // Validate interval and cap the end date to 1 year after the start date + guard interval.end >= interval.start else { + throw WeatherError.invalidRequest("End date must not be earlier than start date") + } + + // Use consistent calendar for date calculations + var calendar = Calendar(identifier: .gregorian) + guard let utcTimeZone = TimeZone(secondsFromGMT: 0) else { + throw WeatherError.invalidRequest("Unable to create UTC timezone") + } + calendar.timeZone = utcTimeZone + + let oneYearAfterStart = calendar.date(byAdding: .year, value: 1, to: interval.start) ?? interval.end + let cappedEndDate = min(interval.end, oneYearAfterStart) + + let startDay = calendar.ordinality(of: .day, in: .year, for: interval.start) ?? 1 + let endDay = calendar.ordinality(of: .day, in: .year, for: cappedEndDate) ?? 366 + + let statistics = try await networkClient.fetchDailyStatistics( + location: location, + dataSets: repeat each including, + startDay: startDay, + endDay: endDay, + jwt: configuration.jwt() + ) + + return (repeat statistics.parse(query: each including)) + } + + /// + /// Returns daily weather statistics for the requested location, for each day between 30 days ago and 10 days from now. + /// + /// - Parameters: + /// - location: The requested location. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested daily weather statistics. + /// + /// The statistics returned for each day are derived from weather data recorded over the past decades, to the present date. Each item returned represents statistics for a particular day of the year, in UTC. For example, if December 31, UTC time, is within the span, the statistics returned for that particular day will be taken from data recorded over the years for day 365 of the year, or 366 if December 31 of the span falls on a leap year. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. Here's an example: + /// + /// ``` + /// let (dailyPrecipitationStatistics, dailyTemperatureStatistics) = try await service.dailyStatistics(for: newYork, including: .precipitation, .temperature) + /// ``` + /// + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func dailyStatistics( + for location: LocationProtocol, + including: repeat DailyWeatherStatisticsQuery + ) async throws -> (repeat DailyWeatherStatistics) { + let now = Date() + let calendar = Calendar.current + let startDate = calendar.date(byAdding: .day, value: -30, to: now) ?? now.addingTimeInterval(-30 * 24 * 60 * 60) + let endDate = calendar.date(byAdding: .day, value: 10, to: now) ?? now.addingTimeInterval(10 * 24 * 60 * 60) + let interval = DateInterval(start: startDate, end: endDate) + + return try await dailyStatistics( + for: location, + forDaysIn: interval, + including: repeat each including + ) + } + + /// + /// Returns hourly weather statistics for the requested location, for each hour from the start hour to the end hour, inclusively. + /// + /// - Parameters: + /// - location: The requested location. + /// - startHour: The first hour of the span, between 1 and 8784. + /// - endHour: The last hour of the span, between 1 and 8784. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested hourly weather statistics. + /// + /// The statistics returned for each hour are derived from weather data recorded over the past decades, to the present date. Each item returned represents statistics for a particular hour of the year, in UTC. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. + /// The following example will return statistics for the first 24 hours of the year. + /// + /// ```swift + /// let hourlyTemperatureStatistics = try await service.hourlyStatistics(for: newYork, startHour: 1, endHour: 24, including: .temperature) + /// ``` + /// + /// If the start hour is greater than the end hour, then a wrap around will occur. This next example will return statistics for hours 8783, 8784, 1, and 2. + /// + /// ```swift + /// let hourlyTemperatureStatistics = try await service.hourlyStatistics(for: newYork, startHour: 8783, endHour: 2, including: .temperature) + /// ``` + /// + /// - Precondition: `startHour in 1...8784 && endHour in 1...8784` + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func hourlyStatistics( + for location: LocationProtocol, + startHour: Int, + endHour: Int, + including: repeat HourlyWeatherStatisticsQuery + ) async throws -> (repeat HourlyWeatherStatistics) { + // Validate hour range + guard (1...8784).contains(startHour) && (1...8784).contains(endHour) && startHour <= endHour else { + throw WeatherError.invalidRequest("Hour range must be between 1-8784 and startHour must be <= endHour") + } + + let statistics = try await networkClient.fetchHourlyStatistics( + location: location, + dataSets: repeat each including, + startHour: startHour, + endHour: endHour, + jwt: configuration.jwt() + ) + + return (repeat statistics.parse(query: each including)) + } + + /// + /// Returns hourly weather statistics for the requested location, for each hour within the specified date interval. + /// + /// - Parameters: + /// - location: The requested location. + /// - interval: The date interval for which to obtain hourly weather statistics. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested hourly weather statistics. + /// + /// The statistics returned for each hour are derived from weather data recorded over the past decades, to the present date. Each item returned represents statistics for a particular hour of the year, in UTC. For example, if the hours of December 31, UTC time, are within the span, the statistics returned for those particular hours will be taken from data recorded over the years for hours 8737 to 8760 of the year, or 8761 to 8784 if December 31 of the span falls on a leap year. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. Here's an example: + /// + /// ```swift + /// let hourlyTemperatureStatistics = try await service.hourlyStatistics(for: newYork, forHoursIn: interval, including: .temperature) + /// ``` + /// + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func hourlyStatistics( + for location: LocationProtocol, + forHoursIn interval: DateInterval, + including: repeat HourlyWeatherStatisticsQuery + ) async throws -> (repeat HourlyWeatherStatistics) { + // Convert DateInterval to hour of year + let calendar = Calendar.current + let startOfYear = calendar.date(from: calendar.dateComponents([.year], from: interval.start))! + let startHour = calendar.dateComponents([.hour], from: startOfYear, to: interval.start).hour ?? 0 + let endHour = calendar.dateComponents([.hour], from: startOfYear, to: interval.end).hour ?? 8783 + + return try await hourlyStatistics( + for: location, + startHour: max(1, startHour + 1), + endHour: min(8784, endHour + 1), + including: repeat each including + ) + } + + /// + /// Returns hourly weather statistics for the requested location, for the 24 hours of the current day. + /// + /// - Parameters: + /// - location: The requested location. + /// - interval: The date interval for which to obtain hourly weather statistics. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested hourly weather statistics. + /// + /// The statistics returned for each hour are derived from weather data recorded over the past decades, to the present date. Each item returned represents statistics for a particular hour of the year, in UTC. For example, if the hours of December 31, UTC time, are within the span, the statistics returned for those particular hours will be taken from data recorded over the years for hours 8737 to 8760 of the year, or 8761 to 8784 if December 31 of the span falls on a leap year. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. Here's an example: + /// + /// ```swift + /// let hourlyTemperatureStatistics = try await service.hourlyStatistics(for: newYork, including: .temperature) + /// ``` + /// + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func hourlyStatistics( + for location: LocationProtocol, + including: repeat HourlyWeatherStatisticsQuery + ) async throws -> (repeat HourlyWeatherStatistics) { + let now = Date() + let calendar = Calendar.current + let startOfDay = calendar.startOfDay(for: now) + let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay) ?? startOfDay.addingTimeInterval(24 * 60 * 60) + let interval = DateInterval(start: startOfDay, end: endOfDay) + + return try await hourlyStatistics( + for: location, + forHoursIn: interval, + including: repeat each including + ) + } + + /// + /// Returns monthly weather statistics for the requested location, for each month from the start month to the end month, inclusively. + /// + /// - Parameters: + /// - location: The requested location. + /// - startMonth: The first month of the span, between 1 and 12. + /// - endMonth: The last month of the span, between 1 and 12. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested monthly weather statistics. + /// + /// The statistics returned for each month are derived from weather data recorded over the past decades, to the present date. Each item returned represents statistics for a particular Gregorian Calendar month of the year. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. + /// The following example will return statistics for all months of the year. + /// + /// ```swift + /// let (monthlyPrecipitationStatistics, monthlyTemperatureStatistics) = try await service.monthlyStatistics(for: newYork, startMonth: 1, endMonth: 12, including: .precipitation, .temperature) + /// ``` + /// + /// If `start` comes after `end` in the year, then a wrap around will occur. This next example will return statistics for months 11, 12, 1, and 2. + /// + /// ```swift + /// let (monthlyPrecipitationStatistics, monthlyTemperatureStatistics) = try await service.monthlyStatistics(for: newYork, startMonth: 11, endMonth: 2, including: .precipitation, .temperature) + /// ``` + /// + /// - Precondition: `startMonth in 1...12 && endMonth in 1...12` + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func monthlyStatistics( + for location: LocationProtocol, + startMonth: Int, + endMonth: Int, + including: repeat MonthlyWeatherStatisticsQuery + ) async throws -> (repeat MonthlyWeatherStatistics) { + // Validate month range + guard (1...12).contains(startMonth) && (1...12).contains(endMonth) && startMonth <= endMonth else { + throw WeatherError.invalidRequest("Month range must be between 1-12 and startMonth must be <= endMonth") + } + + let statistics = try await networkClient.fetchMonthlyStatistics( + location: location, + dataSets: repeat each including, + startMonth: startMonth, + endMonth: endMonth, + jwt: configuration.jwt() + ) + + return (repeat statistics.parse(query: each including)) + } + + /// + /// Returns monthly weather statistics for the requested location, for each month within the specified date interval. + /// + /// - Parameters: + /// - location: The requested location. + /// - interval: The date interval for which to obtain monthly weather statistics. The end date of the interval will be capped at 1 year after the start date. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested monthly weather statistics. + /// + /// The statistics returned for each month are derived from weather data recorded over the past decades, to the present date. Each item returned represents statistics for a particular Gregorian Calendar month of the year. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. Here's an example: + /// + /// ```swift + /// let (monthlyPrecipitationStatistics, monthlyTemperatureStatistics) = try await service.monthlyStatistics(for: newYork, forMonthsIn: interval, including: .precipitation, .temperature) + /// ``` + /// + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func monthlyStatistics( + for location: LocationProtocol, + forMonthsIn interval: DateInterval, + including: repeat MonthlyWeatherStatisticsQuery + ) async throws -> (repeat MonthlyWeatherStatistics) { + // Convert DateInterval to month range + let calendar = Calendar.current + let startMonth = calendar.component(.month, from: interval.start) + let endMonth = calendar.component(.month, from: interval.end) + + return try await monthlyStatistics( + for: location, + startMonth: startMonth, + endMonth: endMonth, + including: repeat each including + ) + } + + /// + /// Returns monthly weather statistics for the requested location, for all 12 months of the Gregorian calendar year. + /// + /// - Parameters: + /// - location: The requested location. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested monthly weather statistics + /// + /// The statistics returned for each month are derived from weather data recorded over the past decades, to the present date. Each item returned represents statistics for a particular Gregorian Calendar month of the year. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. Here's an example: + /// + /// ```swift + /// let (monthlyPrecipitationStatistics, monthlyTemperatureStatistics) = try await service.monthlyStatistics(for: newYork, including: .precipitation, .temperature) + /// ``` + /// + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func monthlyStatistics( + for location: LocationProtocol, + including: repeat MonthlyWeatherStatisticsQuery + ) async throws -> (repeat MonthlyWeatherStatistics) { + try await monthlyStatistics( + for: location, + startMonth: 1, + endMonth: 12, + including: repeat each including + ) + } + +} diff --git a/Sources/OpenWeatherKit/Public/WeatherService+Summary.swift b/Sources/OpenWeatherKit/Public/WeatherService+Summary.swift new file mode 100644 index 0000000..c8cd852 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/WeatherService+Summary.swift @@ -0,0 +1,164 @@ +// +// WeatherService+Summary.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/30/25. +// + +import Foundation + +// MARK: - Daily Summary +extension WeatherService { + + /// + /// Returns daily weather statistics for the requested location, for each day from the start day to the end day, inclusively. + /// + /// - Parameters: + /// - location: The requested location. + /// - startDay: The first day of the span, between 1 and 366. + /// - endDay: The last day of the span, between 1 and 366. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested daily weather statistics + /// + /// The statistics returned for each day are derived from weather data recorded over the past decades, to the present date. Each item returned represents statistics for a particular day of the year, in UTC. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. The following example will return statistics for the first 10 days of the year. + /// + /// ``` + /// let (dailyPrecipitationStatistics, dailyTemperatureStatistics) = try await service.dailyStatistics(for: newYork, startDay: 1, endDay: 10, including: .precipitation, .temperature) + /// ``` + /// + /// If `startDay` is greater than `endDay`, then a wrap around will occur. This next example will return statistics for days 365, 366, 1, and 2. + /// + /// ```swift + /// let (dailyPrecipitationStatistics, dailyTemperatureStatistics) = try await service.dailyStatistics(for: newYork, startDay: 365, endDay: 2, including: .precipitation, .temperature) + /// ``` + /// + /// - Precondition: `startDay in 1...366 && endDay in 1...366` + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func dailySummary( + for location: LocationProtocol, + startDay: Int, + endDay: Int, + including: repeat DailyWeatherSummaryQuery + ) async throws -> (repeat DailyWeatherSummary) { + // Use a UTC Gregorian calendar to interpret day-of-year in UTC + var calendar = Calendar(identifier: .gregorian) + guard let utcTimeZone = TimeZone(secondsFromGMT: 0) else { + throw WeatherError.invalidRequest("Unable to create UTC timezone") + } + calendar.timeZone = utcTimeZone + + let now = Date() + let currentYear = calendar.component(.year, from: now) + + // Compute the date range + let (startDate, endDate) = try Date.computeDateRange( + startDay: startDay, + endDay: endDay, + calendar: calendar, + referenceYear: currentYear + ) + + // Build interval and delegate to date-interval overload; header docs note end is capped at 1 year after start + let interval = DateInterval(start: startDate, end: endDate) + return try await dailySummary( + for: location, + forDaysIn: interval, + including: repeat each including + ) + } + + /// + /// Returns day weather summaries for the requested location, for each day within the provided date interval. + /// + /// - Parameters: + /// - location: The requested location. + /// - interval: The date interval for which to obtain day weather summaries. The end date of the interval will be capped at 1 year after the start date. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested day weather summaries. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. The following example will get a daily weather summary for New York City. + /// + /// ```swift + /// let (dailyPrecipitationSummary, dailyTemperatureSummary) = try await service.dailySummary(for: newYork, forDaysIn: timeInterval, including: .precipitation, .temperature) + /// ``` + /// + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func dailySummary( + for location: LocationProtocol, + forDaysIn interval: DateInterval, + including: repeat DailyWeatherSummaryQuery + ) async throws -> (repeat DailyWeatherSummary) { + // Validate interval and cap the end date to 1 year after the start date + guard interval.end >= interval.start else { + throw WeatherError.invalidRequest("End date must not be earlier than start date") + } + + // Use consistent calendar for date calculations + var calendar = Calendar(identifier: .gregorian) + guard let utcTimeZone = TimeZone(secondsFromGMT: 0) else { + throw WeatherError.invalidRequest("Unable to create UTC timezone") + } + calendar.timeZone = utcTimeZone + + let oneYearAfterStart = calendar.date(byAdding: .year, value: 1, to: interval.start) ?? interval.end + let cappedEndDate = min(interval.end, oneYearAfterStart) + + let summary = try await networkClient.fetchDailySummary( + location: location, + dataSets: repeat each including, + startDate: interval.start, + endDate: cappedEndDate, + jwt: configuration.jwt() + ) + + return (repeat summary.parse(query: each including)) + } + + + /// + /// Returns day weather summaries for the requested location, for the past 30 days, including the present day. + /// + /// - Parameters: + /// - location: The requested location. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested day weather summaries. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. The following example will get a daily weather summary for New York City. + /// + /// ```swift + /// let (dailyPrecipitationSummary, dailyTemperatureSummary) = try await service.dailySummary(for: newYork, including: .precipitation, .temperature) + /// ``` + /// + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inlinable + final public func dailySummary( + for location: LocationProtocol, + including: repeat DailyWeatherSummaryQuery + ) async throws -> (repeat DailyWeatherSummary) { + // Build a 30-day interval ending today (including the present day) + let endDate = Date() + + // Use consistent calendar for date calculations + var calendar = Calendar(identifier: .gregorian) + guard let utcTimeZone = TimeZone(secondsFromGMT: 0) else { + throw WeatherError.invalidRequest("Unable to create UTC timezone") + } + calendar.timeZone = utcTimeZone + + guard let startDate = calendar.date(byAdding: .day, value: -30, to: endDate) else { + throw WeatherError.invalidRequest("Unable to compute 30-day interval") + } + + let interval = DateInterval(start: startDate, end: endDate) + return try await dailySummary( + for: location, + forDaysIn: interval, + including: repeat each including + ) + } +} + diff --git a/Sources/OpenWeatherKit/Public/WeatherService.swift b/Sources/OpenWeatherKit/Public/WeatherService.swift index 0adfd34..9874255 100644 --- a/Sources/OpenWeatherKit/Public/WeatherService.swift +++ b/Sources/OpenWeatherKit/Public/WeatherService.swift @@ -105,405 +105,4 @@ final public class WeatherService: Sendable { combinedMarkLightURL: URL(string: "https://weather-data.apple.com/assets/branding/combined-mark-light.png")!) } } - -#if canImport(CoreLocation) - /// - /// Returns the weather forecast for the requested location. Includes all available weather data sets. - /// - Parameter location: The requested location. - /// - Throws: Weather data error `WeatherError` - /// - Returns: The aggregate weather. - /// - @inlinable - final public func weather(for location: LocationProtocol) async throws -> Weather { - let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await getWeather(location: location, countryCode: countryCode, timezone: timezone) - } -#endif - - /// - /// Returns the weather forecast for the requested location. Includes all available weather data sets. - /// - Parameter location: The requested location. - /// - Throws: Weather data error `WeatherError` - /// - Returns: The aggregate weather. - /// - @inlinable - final public func weather( - for location: LocationProtocol, - countryCode: String, - timezone: TimeZone, - language: WeatherService.Configuration.Language? = nil - ) async throws -> Weather { - try await getWeather(location: location, countryCode: countryCode, timezone: timezone, language: language) - } - - /// - /// Returns the weather forecast for the requested location. - /// - Parameters: - /// - location: The requested location. - /// - including: Weather query - /// - Throws: Weather data error `WeatherError` - /// - Returns: The requested weather data set. - /// - /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. - /// - /// Example usage: - /// `let current = try await service.weather(for: newYork, including: .current)` - /// -#if canImport(CoreLocation) - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet: WeatherQuery - ) async throws -> T { - let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet, countryCode: countryCode, timezone: timezone) - } -#endif - - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet: WeatherQuery, - countryCode: String? = nil, - timezone: TimeZone - ) async throws -> T { - let _dataSet = countryCode.map { dataSet.update(with: $0) } ?? dataSet - - let proxy = try await networkClient.fetchWeather( - location: location, - language: self.configuration.language, - queries: _dataSet, - timezone: timezone, - jwt: self.configuration.jwt() - ) - - return try _dataSet.result(proxy) - } - - /// - /// Returns the weather forecast for the requested location. - /// - Parameters: - /// - location: The requested location. - /// - including: Weather queries - /// - Throws: Weather data error `WeatherError` - /// - Returns: The requested weather data set. - /// - /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. - /// - /// Example usage: - /// `let (current, minute) = try await service.weather(for: newYork, including: .current, .minute)` - /// -#if canImport(CoreLocation) - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet1: WeatherQuery, - _ dataSet2: WeatherQuery - ) async throws -> (T1, T2) { - let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet1, dataSet2, countryCode: countryCode, timezone: timezone) - } -#endif - - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet1: WeatherQuery, - _ dataSet2: WeatherQuery, - countryCode: String? = nil, - timezone: TimeZone - ) async throws -> (T1, T2) { - let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 - let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 - - let proxy = try await networkClient.fetchWeather( - location: location, - language: self.configuration.language, - queries: _dataSet1, _dataSet2, - timezone: timezone, - jwt: self.configuration.jwt() - ) - - return try ( - _dataSet1.result(proxy), - _dataSet2.result(proxy) - ) - } - - /// - /// Returns the weather forecast for the requested location. - /// - Parameters: - /// - location: The requested location. - /// - including: Weather queries - /// - Throws: Weather data error `WeatherError` - /// - Returns: The requested weather data set. - /// - /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. - /// -#if canImport(CoreLocation) - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet1: WeatherQuery, - _ dataSet2: WeatherQuery, - _ dataSet3: WeatherQuery - ) async throws -> (T1, T2, T3) { - let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, countryCode: countryCode, timezone: timezone) - } -#endif - - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet1: WeatherQuery, - _ dataSet2: WeatherQuery, - _ dataSet3: WeatherQuery, - countryCode: String? = nil, - timezone: TimeZone - ) async throws -> (T1, T2, T3) { - let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 - let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 - let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 - - let proxy = try await networkClient.fetchWeather( - location: location, - language: self.configuration.language, - queries: _dataSet1, _dataSet2, _dataSet3, - timezone: timezone, - jwt: self.configuration.jwt() - ) - - return try ( - _dataSet1.result(proxy), - _dataSet2.result(proxy), - _dataSet3.result(proxy) - ) - } - - /// - /// Returns the weather forecast for the requested location. - /// - Parameters: - /// - location: The requested location. - /// - including: Weather queries - /// - Throws: Weather data error `WeatherError` - /// - Returns: The requested weather data set. - /// - /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. - /// -#if canImport(CoreLocation) - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet1: WeatherQuery, - _ dataSet2: WeatherQuery, - _ dataSet3: WeatherQuery, - _ dataSet4: WeatherQuery - ) async throws -> (T1, T2, T3, T4) { - let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, countryCode: countryCode, timezone: timezone) - } -#endif - - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet1: WeatherQuery, - _ dataSet2: WeatherQuery, - _ dataSet3: WeatherQuery, - _ dataSet4: WeatherQuery, - countryCode: String? = nil, - timezone: TimeZone - ) async throws -> (T1, T2, T3, T4) { - let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 - let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 - let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 - let _dataSet4 = countryCode.map { dataSet4.update(with: $0) } ?? dataSet4 - - let proxy = try await networkClient.fetchWeather( - location: location, - language: self.configuration.language, - queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, - timezone: timezone, - jwt: self.configuration.jwt() - ) - - return try ( - _dataSet1.result(proxy), - _dataSet2.result(proxy), - _dataSet3.result(proxy), - _dataSet4.result(proxy) - ) - } - - /// - /// Returns the weather forecast for the requested location. - /// - Parameters: - /// - location: The requested location. - /// - including: Weather queries - /// - Throws: Weather data error `WeatherError` - /// - Returns: The requested weather data set. - /// - /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. - /// -#if canImport(CoreLocation) - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet1: WeatherQuery, - _ dataSet2: WeatherQuery, - _ dataSet3: WeatherQuery, - _ dataSet4: WeatherQuery, - _ dataSet5: WeatherQuery - ) async throws -> (T1, T2, T3, T4, T5) { - let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, dataSet5, countryCode: countryCode, timezone: timezone) - } -#endif - - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet1: WeatherQuery, - _ dataSet2: WeatherQuery, - _ dataSet3: WeatherQuery, - _ dataSet4: WeatherQuery, - _ dataSet5: WeatherQuery, - countryCode: String? = nil, - timezone: TimeZone - ) async throws -> (T1, T2, T3, T4, T5) { - let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 - let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 - let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 - let _dataSet4 = countryCode.map { dataSet4.update(with: $0) } ?? dataSet4 - let _dataSet5 = countryCode.map { dataSet5.update(with: $0) } ?? dataSet5 - - let proxy = try await networkClient.fetchWeather( - location: location, - language: self.configuration.language, - queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, _dataSet5, - timezone: timezone, - jwt: self.configuration.jwt() - ) - - return try ( - _dataSet1.result(proxy), - _dataSet2.result(proxy), - _dataSet3.result(proxy), - _dataSet4.result(proxy), - _dataSet5.result(proxy) - ) - } - - /// - /// Returns the weather forecast for the requested location. - /// - Parameters: - /// - location: The requested location. - /// - including: Weather queries - /// - Throws: Weather data error `WeatherError` - /// - Returns: The requested weather data set. - /// - /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. - /// -#if canImport(CoreLocation) - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet1: WeatherQuery, - _ dataSet2: WeatherQuery, - _ dataSet3: WeatherQuery, - _ dataSet4: WeatherQuery, - _ dataSet5: WeatherQuery, - _ dataSet6: WeatherQuery - ) async throws -> (T1, T2, T3, T4, T5, T6) { - let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, dataSet5, dataSet6, countryCode: countryCode, timezone: timezone) - } -#endif - - @inlinable - final public func weather( - for location: LocationProtocol, - including dataSet1: WeatherQuery, - _ dataSet2: WeatherQuery, - _ dataSet3: WeatherQuery, - _ dataSet4: WeatherQuery, - _ dataSet5: WeatherQuery, - _ dataSet6: WeatherQuery, - countryCode: String? = nil, - timezone: TimeZone - ) async throws -> (T1, T2, T3, T4, T5, T6) { - let _dataSet1 = countryCode.map { dataSet1.update(with: $0) } ?? dataSet1 - let _dataSet2 = countryCode.map { dataSet2.update(with: $0) } ?? dataSet2 - let _dataSet3 = countryCode.map { dataSet3.update(with: $0) } ?? dataSet3 - let _dataSet4 = countryCode.map { dataSet4.update(with: $0) } ?? dataSet4 - let _dataSet5 = countryCode.map { dataSet5.update(with: $0) } ?? dataSet5 - let _dataSet6 = countryCode.map { dataSet6.update(with: $0) } ?? dataSet6 - - let proxy = try await networkClient.fetchWeather( - location: location, - language: self.configuration.language, - queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, _dataSet5, _dataSet6, - timezone: timezone, - jwt: self.configuration.jwt() - ) - - return try ( - _dataSet1.result(proxy), - _dataSet2.result(proxy), - _dataSet3.result(proxy), - _dataSet4.result(proxy), - _dataSet5.result(proxy), - _dataSet6.result(proxy) - ) - } -} - -extension WeatherService { -#if canImport(CoreLocation) - @usableFromInline - func resolveCountryCodeAndTimezone(for location: LocationProtocol) async throws -> (countryCode: String, timezone: TimeZone) { - guard let countryCode = try await geocoder.countryCode(location) else { - throw WeatherError.countryCode - } - guard let timezoneIdentifier = try await geocoder.timezone(location), - let timezone = TimeZone(identifier: timezoneIdentifier) else { - throw WeatherError.timezone - } - return (countryCode, timezone) - } -#endif - - @usableFromInline - func getWeather(location: LocationProtocol, countryCode: String, timezone: TimeZone, language: WeatherService.Configuration.Language? = nil) async throws -> Weather { - let proxy = try await networkClient.fetchWeather( - location: location, - language: language ?? self.configuration.language, - queries: WeatherQuery.current, - WeatherQuery?>.minute, - WeatherQuery>.hourly, - WeatherQuery>.daily, - WeatherQuery<[WeatherAlert]?>.alerts(countryCode: countryCode), - WeatherQuery.availability(countryCode: countryCode), - timezone: timezone, - jwt: self.configuration.jwt() - ) - - return try Weather( - currentWeather: proxy.currentWeather.unwrap( - or: WeatherError.missingData(APIWeather.CodingKeys.currentWeather.rawValue) - ), - minuteForecast: proxy.minuteForecast, - hourlyForecast: proxy.hourlyForecast.unwrap( - or: WeatherError.missingData(APIWeather.CodingKeys.forecastHourly.rawValue) - ), - dailyForecast: proxy.dailyForecast.unwrap( - or: WeatherError.missingData(APIWeather.CodingKeys.forecastDaily.rawValue) - ), - weatherAlerts: proxy.weatherAlerts, - availability: proxy.availability.unwrap( - or: WeatherError.missingData(QueryContants.availability) - ) - ) - } } diff --git a/Tests/OpenWeatherKitTests/WeatherServiceDateRangeTests.swift b/Tests/OpenWeatherKitTests/WeatherServiceDateRangeTests.swift new file mode 100644 index 0000000..a0404ad --- /dev/null +++ b/Tests/OpenWeatherKitTests/WeatherServiceDateRangeTests.swift @@ -0,0 +1,299 @@ +// +// WeatherServiceDateRangeTests.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/30/25. +// + +#if canImport(Testing) +import Testing +import Foundation +@testable import OpenWeatherKit + +@Suite("WeatherService Date Range Computation Tests") +struct WeatherServiceDateRangeTests { + + /// Creates a UTC Gregorian calendar for testing + private var utcCalendar: Calendar { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + return calendar + } + + // MARK: - Normal Cases (No Wrap-Around) + + @Test("Normal case: start and end in same year") + func testNormalCase() throws { + let calendar = utcCalendar + let referenceYear = 2024 + + // January 15 to January 31 + let result = try Date.computeDateRange( + startDay: 15, + endDay: 31, + calendar: calendar, + referenceYear: referenceYear + ) + + let expectedStart = calendar.date(from: DateComponents(year: 2024, month: 1, day: 15))! + let expectedEnd = calendar.date(from: DateComponents(year: 2024, month: 1, day: 31))! + + #expect(result.startDate == expectedStart) + #expect(result.endDate == expectedEnd) + } + + @Test("First and last day of year") + func testFirstAndLastDay() throws { + let calendar = utcCalendar + let referenceYear = 2024 + + // Day 1 to Day 365 + let result = try Date.computeDateRange( + startDay: 1, + endDay: 365, + calendar: calendar, + referenceYear: referenceYear + ) + + let expectedStart = calendar.date(from: DateComponents(year: 2024, month: 1, day: 1))! + let expectedEnd = calendar.date(from: DateComponents(year: 2024, month: 12, day: 30))! // 2024 is leap year + + #expect(result.startDate == expectedStart) + #expect(result.endDate == expectedEnd) + } + + @Test("Same start and end day") + func testSameDay() throws { + let calendar = utcCalendar + let referenceYear = 2024 + + // Day 100 to Day 100 + let result = try Date.computeDateRange( + startDay: 100, + endDay: 100, + calendar: calendar, + referenceYear: referenceYear + ) + + let expectedDate = calendar.date(from: DateComponents(year: 2024, month: 4, day: 9))! + + #expect(result.startDate == expectedDate) + #expect(result.endDate == expectedDate) + } + + // MARK: - Wrap-Around Cases + + @Test("Wrap-around: December 31 to January 2") + func testWrapAroundDecemberToJanuary() throws { + let calendar = utcCalendar + let referenceYear = 2025 + + // Day 366 (Dec 31 in leap year) to Day 2 (Jan 2) + let result = try Date.computeDateRange( + startDay: 366, + endDay: 2, + calendar: calendar, + referenceYear: referenceYear + ) + + let expectedStart = calendar.date(from: DateComponents(year: 2024, month: 12, day: 31))! + let expectedEnd = calendar.date(from: DateComponents(year: 2025, month: 1, day: 2))! + + #expect(result.startDate == expectedStart) + #expect(result.endDate == expectedEnd) + } + + @Test("Wrap-around: Day 365 to Day 1") + func testWrapAroundLastToFirst() throws { + let calendar = utcCalendar + let referenceYear = 2024 + + // Day 365 to Day 1 + let result = try Date.computeDateRange( + startDay: 365, + endDay: 1, + calendar: calendar, + referenceYear: referenceYear + ) + + let expectedStart = calendar.date(from: DateComponents(year: 2023, month: 12, day: 31))! // 2023 is not leap year + let expectedEnd = calendar.date(from: DateComponents(year: 2024, month: 1, day: 1))! + + #expect(result.startDate == expectedStart) + #expect(result.endDate == expectedEnd) + } + + @Test("Wrap-around: Mid-year to early year") + func testWrapAroundMidToEarly() throws { + let calendar = utcCalendar + let referenceYear = 2024 + + // Day 200 to Day 50 + let result = try Date.computeDateRange( + startDay: 200, + endDay: 50, + calendar: calendar, + referenceYear: referenceYear + ) + + let expectedStart = calendar.date(from: DateComponents(year: 2023, month: 7, day: 19))! + let expectedEnd = calendar.date(from: DateComponents(year: 2024, month: 2, day: 19))! + + #expect(result.startDate == expectedStart) + #expect(result.endDate == expectedEnd) + } + + // MARK: - Leap Year Tests + + @Test("Leap year: Day 366 exists") + func testLeapYearDay366() throws { + let calendar = utcCalendar + let referenceYear = 2024 // 2024 is a leap year + + // Day 366 (Dec 31) to Day 366 (same day) + let result = try Date.computeDateRange( + startDay: 366, + endDay: 366, + calendar: calendar, + referenceYear: referenceYear + ) + + let expectedDate = calendar.date(from: DateComponents(year: 2024, month: 12, day: 31))! + + #expect(result.startDate == expectedDate) + #expect(result.endDate == expectedDate) + } + + @Test("Non-leap year: Day 366 should fail") + func testNonLeapYearDay366() throws { + let calendar = utcCalendar + let referenceYear = 2023 // 2023 is not a leap year + + // Day 366 should not exist in 2023 + #expect(throws: WeatherError.self) { + try Date.computeDateRange( + startDay: 366, + endDay: 366, + calendar: calendar, + referenceYear: referenceYear + ) + } + } + + @Test("Wrap-around with leap year boundary") + func testWrapAroundLeapYearBoundary() throws { + let calendar = utcCalendar + let referenceYear = 2024 // 2024 is leap year, 2023 is not + + // Day 366 in 2023 (non-leap year) should fail + #expect(throws: WeatherError.self) { + try Date.computeDateRange( + startDay: 366, + endDay: 1, + calendar: calendar, + referenceYear: referenceYear + ) + } + } + + // MARK: - Edge Cases and Error Conditions + + @Test("Invalid day 0 should fail") + func testInvalidDay0() throws { + let calendar = utcCalendar + let referenceYear = 2024 + + #expect(throws: WeatherError.self) { + try Date.computeDateRange( + startDay: 0, + endDay: 1, + calendar: calendar, + referenceYear: referenceYear + ) + } + } + + @Test("Invalid day 367 should fail") + func testInvalidDay367() throws { + let calendar = utcCalendar + let referenceYear = 2024 + + #expect(throws: WeatherError.self) { + try Date.computeDateRange( + startDay: 1, + endDay: 367, + calendar: calendar, + referenceYear: referenceYear + ) + } + } + + @Test("Extreme past year should work") + func testExtremePastYear() throws { + let calendar = utcCalendar + let referenceYear = 1000 + + let result = try Date.computeDateRange( + startDay: 1, + endDay: 31, + calendar: calendar, + referenceYear: referenceYear + ) + + let expectedStart = calendar.date(from: DateComponents(year: 1000, month: 1, day: 1))! + let expectedEnd = calendar.date(from: DateComponents(year: 1000, month: 1, day: 31))! + + #expect(result.startDate == expectedStart) + #expect(result.endDate == expectedEnd) + } + + @Test("Future year should work") + func testFutureYear() throws { + let calendar = utcCalendar + let referenceYear = 2050 + + let result = try Date.computeDateRange( + startDay: 100, + endDay: 200, + calendar: calendar, + referenceYear: referenceYear + ) + + let expectedStart = calendar.date(from: DateComponents(year: 2050, month: 4, day: 10))! + let expectedEnd = calendar.date(from: DateComponents(year: 2050, month: 7, day: 19))! + + #expect(result.startDate == expectedStart) + #expect(result.endDate == expectedEnd) + } + + // MARK: - Timezone Verification + + @Test("Results should be in UTC timezone") + func testUTCTimezone() throws { + let calendar = utcCalendar + let referenceYear = 2024 + + let result = try Date.computeDateRange( + startDay: 1, + endDay: 2, + calendar: calendar, + referenceYear: referenceYear + ) + + // Verify the dates are interpreted in UTC + let utcComponents1 = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: result.startDate) + let utcComponents2 = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: result.endDate) + + #expect(utcComponents1.year == 2024) + #expect(utcComponents1.month == 1) + #expect(utcComponents1.day == 1) + #expect(utcComponents1.hour == 0) + #expect(utcComponents1.minute == 0) + #expect(utcComponents1.second == 0) + + #expect(utcComponents2.year == 2024) + #expect(utcComponents2.month == 1) + #expect(utcComponents2.day == 2) + } +} +#endif From fb58c2ff492fa0a7e851f719c69cbe980517c118 Mon Sep 17 00:00:00 2001 From: Jeremy Greenwood Date: Mon, 3 Nov 2025 16:06:28 -0500 Subject: [PATCH 05/11] Feature/changes and comparison queries (#43) --- .../Extensions/APIDailyStatistics+Map.swift | 4 +- .../Extensions/APIDailySummary+Map.swift | 4 +- .../APIHistoricalComparisons+Map.swift | 83 +++++++++++ .../Extensions/APIHourlyStatistics+Map.swift | 4 +- .../Extensions/APIMonthlyStatistics+Map.swift | 4 +- .../Extensions/APIWeatherChanges+Map.swift | 31 +++++ .../Internal/Models/APIForecastNextHour.swift | 2 +- .../Models/APIHistoricalComparisons.swift | 55 ++++++++ .../Internal/Models/APIWeather.swift | 4 + .../Internal/Models/APIWeatherChanges.swift | 42 ++++++ .../Internal/NetworkClient.swift | 10 +- Sources/OpenWeatherKit/Internal/Query.swift | 8 +- .../Internal/StatisticsQuery.swift | 10 +- .../Internal/WeatherProxy.swift | 17 ++- .../Public/Characteristics/AlertSummary.swift | 2 +- .../Public/Characteristics/UVIndex.swift | 4 +- .../Characteristics/WeatherCondition.swift | 70 +++++----- .../Forecast/HistoricalComparisons.swift | 122 ++++++++++++++++ .../Public/Forecast/WeatherAvailability.swift | 4 +- .../Public/Forecast/WeatherChanges.swift | 74 ++++++++++ .../DailyWeatherStatisticsQuery.swift | 2 +- .../Requests/DailyWeatherSummaryQuery.swift | 2 +- .../HourlyWeatherStatisticsQuery.swift | 2 +- .../MonthlyWeatherStatisticsQuery.swift | 2 +- .../Public/Requests/WeatherQuery.swift | 36 ++++- .../Statistics/DailyWeatherStatistics.swift | 2 +- .../DayPrecipitationStatistics.swift | 2 +- .../Statistics/DayTemperatureStatistics.swift | 2 +- .../HourTemperatureStatistics.swift | 2 +- .../Statistics/HourlyWeatherStatistics.swift | 2 +- .../MonthPrecipitationStatistics.swift | 2 +- .../MonthTemperatureStatistics.swift | 2 +- .../Statistics/MonthlyWeatherStatistics.swift | 2 +- .../Public/Statistics/Percentiles.swift | 2 +- .../Public/Summary/DailyWeatherSummary.swift | 2 +- .../Summary/DayPrecipitationSummary.swift | 2 +- .../Summary/DayTemperatureSummary.swift | 2 +- .../Public/WeatherService+Forecast.swift | 130 +++++++++++++++--- .../Public/WeatherService+Statistics.swift | 18 +-- .../Public/WeatherService+Summary.swift | 6 +- .../Utils/MockClient.swift | 4 +- .../OpenWeatherKitTests/Utils/MockData.swift | 16 +++ 42 files changed, 678 insertions(+), 118 deletions(-) create mode 100644 Sources/OpenWeatherKit/Internal/Extensions/APIHistoricalComparisons+Map.swift create mode 100644 Sources/OpenWeatherKit/Internal/Extensions/APIWeatherChanges+Map.swift create mode 100644 Sources/OpenWeatherKit/Internal/Models/APIHistoricalComparisons.swift create mode 100644 Sources/OpenWeatherKit/Internal/Models/APIWeatherChanges.swift create mode 100644 Sources/OpenWeatherKit/Public/Forecast/HistoricalComparisons.swift create mode 100644 Sources/OpenWeatherKit/Public/Forecast/WeatherChanges.swift diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIDailyStatistics+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIDailyStatistics+Map.swift index d9052c8..33445e8 100644 --- a/Sources/OpenWeatherKit/Internal/Extensions/APIDailyStatistics+Map.swift +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIDailyStatistics+Map.swift @@ -7,7 +7,7 @@ import Foundation -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension APIDailyStatistics { var dailyPrecipitationStatistics: DailyWeatherStatistics { DailyWeatherStatistics( @@ -42,7 +42,7 @@ extension APIDailyStatistics { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension APIDailyStatisticsData { var precipitationStatistics: DayPrecipitationStatistics { guard let precipitation else { diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIDailySummary+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIDailySummary+Map.swift index b0d1356..16621f2 100644 --- a/Sources/OpenWeatherKit/Internal/Extensions/APIDailySummary+Map.swift +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIDailySummary+Map.swift @@ -7,7 +7,7 @@ import Foundation -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension APIDailySummary { var dailyPrecipitationSummary: DailyWeatherSummary { DailyWeatherSummary( @@ -40,7 +40,7 @@ extension APIDailySummary { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension APIDailySummaryDay { var precipitationSummary: DayPrecipitationSummary { DayPrecipitationSummary( diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIHistoricalComparisons+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIHistoricalComparisons+Map.swift new file mode 100644 index 0000000..6229724 --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIHistoricalComparisons+Map.swift @@ -0,0 +1,83 @@ +// +// APIHistoricalComparisons+Map.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/31/25. +// + +import Foundation + +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) +extension APIHistoricalComparisons { + var historicalComparisons: HistoricalComparisons { + HistoricalComparisons( + comparisons: comparisons.compactMap { $0.historicalComparison }, + metadata: metadata.weatherMetadata + ) + } +} + +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) +extension APIComparison { + var historicalComparison: HistoricalComparison? { + let deviation = Deviation(rawValue: deviation) ?? .normal + + switch conditionType { + case .temperatureMax: + let baseline = TrendBaseline( + kind: TrendBaseline.Kind(rawValue: baselineType) ?? .mean, + value: Measurement(value: baselineValue, unit: UnitTemperature.celsius), + startDate: baselineStartDate + ) + + let trend = Trend( + baseline: baseline, + currentValue: Measurement(value: currentValue, unit: UnitTemperature.celsius), + deviation: deviation + ) + return .highTemperature(trend) + + case .temperatureMin: + let baseline = TrendBaseline( + kind: TrendBaseline.Kind(rawValue: baselineType) ?? .mean, + value: Measurement(value: baselineValue, unit: UnitTemperature.celsius), + startDate: baselineStartDate + ) + let trend = Trend( + baseline: baseline, + currentValue: Measurement(value: currentValue, unit: UnitTemperature.celsius), + deviation: deviation + ) + return .lowTemperature(trend) + + case .precipitation: + let baseline = TrendBaseline( + kind: TrendBaseline.Kind(rawValue: baselineType) ?? .mean, + value: Measurement(value: baselineValue, unit: UnitLength.millimeters), + startDate: baselineStartDate + ) + let trend = Trend( + baseline: baseline, + currentValue: Measurement(value: currentValue, unit: UnitLength.millimeters), + deviation: deviation + ) + return .precipitationAmount(trend) + + case .snowfall: + let baseline = TrendBaseline( + kind: TrendBaseline.Kind(rawValue: baselineType) ?? .mean, + value: Measurement(value: baselineValue, unit: UnitLength.millimeters), + startDate: baselineStartDate + ) + let trend = Trend( + baseline: baseline, + currentValue: Measurement(value: currentValue, unit: UnitLength.millimeters), + deviation: deviation + ) + return .snowfallAmount(trend) + + case .unknown: + return nil + } + } +} diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIHourlyStatistics+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIHourlyStatistics+Map.swift index e665dbc..7a5e920 100644 --- a/Sources/OpenWeatherKit/Internal/Extensions/APIHourlyStatistics+Map.swift +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIHourlyStatistics+Map.swift @@ -7,7 +7,7 @@ import Foundation -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension APIHourlyStatistics { @usableFromInline var hourlyTemperatureStatistics: HourlyWeatherStatistics { @@ -32,7 +32,7 @@ extension APIHourlyStatistics { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension APIHourlyStatisticsData { @usableFromInline var temperatureStatistics: HourTemperatureStatistics { diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIMonthlyStatistics+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIMonthlyStatistics+Map.swift index 4146bcf..7c2b2e7 100644 --- a/Sources/OpenWeatherKit/Internal/Extensions/APIMonthlyStatistics+Map.swift +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIMonthlyStatistics+Map.swift @@ -7,7 +7,7 @@ import Foundation -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension APIMonthlyStatistics { var monthlyPrecipitationStatistics: MonthlyWeatherStatistics { MonthlyWeatherStatistics( @@ -42,7 +42,7 @@ extension APIMonthlyStatistics { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension APIMonthlyStatisticsData { var precipitationStatistics: MonthPrecipitationStatistics { guard let precipitation else { diff --git a/Sources/OpenWeatherKit/Internal/Extensions/APIWeatherChanges+Map.swift b/Sources/OpenWeatherKit/Internal/Extensions/APIWeatherChanges+Map.swift new file mode 100644 index 0000000..4e30ccb --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Extensions/APIWeatherChanges+Map.swift @@ -0,0 +1,31 @@ +// +// APIWeatherChanges+Map.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/31/25. +// + +import Foundation + +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) +extension APIWeatherChanges { + var weatherChanges: WeatherChanges { + WeatherChanges( + changes: changes.map { $0.weatherChange }, + metadata: metadata.weatherMetadata + ) + } +} + +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) +extension APIChange { + var weatherChange: WeatherChange { + WeatherChange( + date: forecastStart, + highTemperature: WeatherChange.Direction(rawValue: maxTemperatureChange) ?? .steady, + lowTemperature: WeatherChange.Direction(rawValue: minTemperatureChange) ?? .steady, + dayPrecipitationAmount: WeatherChange.Direction(rawValue: dayPrecipitationChange) ?? .steady, + nightPrecipitationAmount: WeatherChange.Direction(rawValue: nightPrecipitationChange) ?? .steady + ) + } +} diff --git a/Sources/OpenWeatherKit/Internal/Models/APIForecastNextHour.swift b/Sources/OpenWeatherKit/Internal/Models/APIForecastNextHour.swift index fbb9f49..253143d 100644 --- a/Sources/OpenWeatherKit/Internal/Models/APIForecastNextHour.swift +++ b/Sources/OpenWeatherKit/Internal/Models/APIForecastNextHour.swift @@ -45,4 +45,4 @@ struct APICondition: Codable, Equatable { @TextCaseCoding var endCondition: String @TextCaseCoding var forecastToken: String let startTime: Date -} \ No newline at end of file +} diff --git a/Sources/OpenWeatherKit/Internal/Models/APIHistoricalComparisons.swift b/Sources/OpenWeatherKit/Internal/Models/APIHistoricalComparisons.swift new file mode 100644 index 0000000..76015cf --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Models/APIHistoricalComparisons.swift @@ -0,0 +1,55 @@ +// +// APIHistoricalComparisons.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/31/25. +// + +import Foundation + +// MARK: - APIHistoricalComparisons +struct APIHistoricalComparisons: Codable, Equatable, Sendable { + let metadata: APIMetadata + let comparisons: [APIComparison] + + enum CodingKeys: String, CodingKey { + case metadata = "metadata" + case comparisons = "comparisons" + } +} + +// MARK: - APIComparison +struct APIComparison: Codable, Equatable, Sendable { + @TextCaseCoding var baselineType: String + @TextCaseCoding var condition: String + @TextCaseCoding var deviation: String + let baselineStartDate: Date + let baselineValue: Double + let currentValue: Double + + var conditionType: Condition { + guard let condition = Condition(rawValue: condition) else { + assertionFailure("Could not parse comparison condition \(condition)") + return .unknown + } + + return condition + } + + enum CodingKeys: String, CodingKey { + case baselineStartDate = "baselineStartDate" + case baselineType = "baselineType" + case baselineValue = "baselineValue" + case condition = "condition" + case currentValue = "currentValue" + case deviation = "deviation" + } + + enum Condition: String { + case temperatureMax = "temperature_max" + case temperatureMin = "temperature_min" + case precipitation = "precipitation" + case snowfall = "snowfall" + case unknown + } +} diff --git a/Sources/OpenWeatherKit/Internal/Models/APIWeather.swift b/Sources/OpenWeatherKit/Internal/Models/APIWeather.swift index 0e3de7f..ec31cba 100644 --- a/Sources/OpenWeatherKit/Internal/Models/APIWeather.swift +++ b/Sources/OpenWeatherKit/Internal/Models/APIWeather.swift @@ -13,13 +13,17 @@ struct APIWeather: Codable, Equatable { let forecastDaily: APIForecastDaily? let forecastHourly: APIForecastHourly? let forecastNextHour: APIForecastNextHour? + let historicalComparisons: APIHistoricalComparisons? let weatherAlerts: APIWeatherAlerts? + let weatherChanges: APIWeatherChanges? enum CodingKeys: String, CodingKey { case currentWeather = "currentWeather" case forecastDaily = "forecastDaily" case forecastHourly = "forecastHourly" case forecastNextHour = "forecastNextHour" + case historicalComparisons = "historicalComparisons" case weatherAlerts = "weatherAlerts" + case weatherChanges = "weatherChanges" } } diff --git a/Sources/OpenWeatherKit/Internal/Models/APIWeatherChanges.swift b/Sources/OpenWeatherKit/Internal/Models/APIWeatherChanges.swift new file mode 100644 index 0000000..1605eeb --- /dev/null +++ b/Sources/OpenWeatherKit/Internal/Models/APIWeatherChanges.swift @@ -0,0 +1,42 @@ +// +// APIWeatherChanges.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/31/25. +// + +import Foundation + +// MARK: - APIWeatherChanges +struct APIWeatherChanges: Codable, Equatable, Sendable { + let metadata: APIMetadata + let changes: [APIChange] + let forecastEnd: Date + let forecastStart: Date + + enum CodingKeys: String, CodingKey { + case metadata = "metadata" + case changes = "changes" + case forecastEnd = "forecastEnd" + case forecastStart = "forecastStart" + } +} + +// MARK: - APIChange +struct APIChange: Codable, Equatable, Sendable { + @TextCaseCoding var dayPrecipitationChange: String + @TextCaseCoding var maxTemperatureChange: String + @TextCaseCoding var minTemperatureChange: String + @TextCaseCoding var nightPrecipitationChange: String + let forecastEnd: Date + let forecastStart: Date + + enum CodingKeys: String, CodingKey { + case dayPrecipitationChange = "dayPrecipitationChange" + case forecastEnd = "forecastEnd" + case forecastStart = "forecastStart" + case maxTemperatureChange = "maxTemperatureChange" + case minTemperatureChange = "minTemperatureChange" + case nightPrecipitationChange = "nightPrecipitationChange" + } +} diff --git a/Sources/OpenWeatherKit/Internal/NetworkClient.swift b/Sources/OpenWeatherKit/Internal/NetworkClient.swift index ab43f62..4dbf7fe 100644 --- a/Sources/OpenWeatherKit/Internal/NetworkClient.swift +++ b/Sources/OpenWeatherKit/Internal/NetworkClient.swift @@ -34,7 +34,7 @@ struct NetworkClient: Sendable { ) } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @usableFromInline func fetchDailySummary( location: LocationProtocol, @@ -59,7 +59,7 @@ struct NetworkClient: Sendable { ) } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @usableFromInline func fetchHourlyStatistics( location: LocationProtocol, @@ -84,7 +84,7 @@ struct NetworkClient: Sendable { ) } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @usableFromInline func fetchDailyStatistics( location: LocationProtocol, @@ -109,7 +109,7 @@ struct NetworkClient: Sendable { ) } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @usableFromInline func fetchMonthlyStatistics( location: LocationProtocol, @@ -138,7 +138,7 @@ struct NetworkClient: Sendable { func fetchWeather( location: LocationProtocol, language: WeatherService.Configuration.Language, - queries: Query..., + queries: [any Query], timezone: TimeZone, jwt: String ) async throws -> WeatherProxy { diff --git a/Sources/OpenWeatherKit/Internal/Query.swift b/Sources/OpenWeatherKit/Internal/Query.swift index 2f099f4..4b55b2c 100644 --- a/Sources/OpenWeatherKit/Internal/Query.swift +++ b/Sources/OpenWeatherKit/Internal/Query.swift @@ -17,21 +17,25 @@ extension WeatherQuery: Query {} @usableFromInline enum QueryType { case alerts(_ dataSet: String, _ countryCode: String) + case availability(_ dataSet: String, _ countryCode: String) + case changes(_ dataSet: String) + case comparisons(_ dataSet: String) case current(_ dataSet: String) case daily(_ dataSet: String, _ startDate: Date, _ endDate: Date) case hourly(_ dataSet: String, _ startDate: Date, _ endDate: Date) case minute(_ dataSet: String) - case availability(_ dataSet: String, _ countryCode: String) @usableFromInline var dataSet: String { switch self { case let .alerts(dataSet,_): return dataSet + case let .availability(dataSet, _): return dataSet + case let .changes(dataSet): return dataSet + case let .comparisons(dataSet): return dataSet case let .current(dataSet): return dataSet case let .daily(dataSet, _, _): return dataSet case let .hourly(dataSet, _, _): return dataSet case let .minute(dataSet): return dataSet - case let .availability(dataSet, _): return dataSet } } } diff --git a/Sources/OpenWeatherKit/Internal/StatisticsQuery.swift b/Sources/OpenWeatherKit/Internal/StatisticsQuery.swift index 7b67b79..f5a8df0 100644 --- a/Sources/OpenWeatherKit/Internal/StatisticsQuery.swift +++ b/Sources/OpenWeatherKit/Internal/StatisticsQuery.swift @@ -7,7 +7,7 @@ import Foundation -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @usableFromInline protocol StatisticsQuery { var statisticsType: StatisticsType { get } @@ -27,14 +27,14 @@ enum StatisticsType: Sendable { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension DailyWeatherStatisticsQuery: StatisticsQuery {} -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension HourlyWeatherStatisticsQuery: StatisticsQuery {} -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension MonthlyWeatherStatisticsQuery: StatisticsQuery {} -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) extension DailyWeatherSummaryQuery: StatisticsQuery {} diff --git a/Sources/OpenWeatherKit/Internal/WeatherProxy.swift b/Sources/OpenWeatherKit/Internal/WeatherProxy.swift index c54ab10..dce9f8a 100644 --- a/Sources/OpenWeatherKit/Internal/WeatherProxy.swift +++ b/Sources/OpenWeatherKit/Internal/WeatherProxy.swift @@ -15,7 +15,9 @@ struct WeatherProxy: Sendable { dailyForecast: Forecast?, hourlyForecast: Forecast?, minuteForecast: Forecast?, - weatherAlerts: [WeatherAlert]? + weatherAlerts: [WeatherAlert]?, + weatherChanges: WeatherChanges? = nil, + historicalComparisons: HistoricalComparisons? = nil ) { self.availability = availability self.currentWeather = currentWeather @@ -23,6 +25,8 @@ struct WeatherProxy: Sendable { self.hourlyForecast = hourlyForecast self.minuteForecast = minuteForecast self.weatherAlerts = weatherAlerts + self.weatherChanges = weatherChanges + self.historicalComparisons = historicalComparisons } var availability: WeatherAvailability? @@ -32,6 +36,9 @@ struct WeatherProxy: Sendable { var minuteForecast: Forecast? var weatherAlerts: [WeatherAlert]? + var weatherChanges: WeatherChanges? + var historicalComparisons: HistoricalComparisons? + func combined(with weatherProxy: WeatherProxy) -> WeatherProxy { WeatherProxy( availability: availability ?? weatherProxy.availability, @@ -39,7 +46,9 @@ struct WeatherProxy: Sendable { dailyForecast: dailyForecast ?? weatherProxy.dailyForecast, hourlyForecast: hourlyForecast ?? weatherProxy.hourlyForecast, minuteForecast: minuteForecast ?? weatherProxy.minuteForecast, - weatherAlerts: weatherAlerts ?? weatherProxy.weatherAlerts + weatherAlerts: weatherAlerts ?? weatherProxy.weatherAlerts, + weatherChanges: weatherChanges ?? weatherProxy.weatherChanges, + historicalComparisons: historicalComparisons ?? weatherProxy.historicalComparisons ) } } @@ -51,6 +60,8 @@ extension WeatherProxy { dailyForecast: nil, hourlyForecast: nil, minuteForecast: nil, - weatherAlerts: nil + weatherAlerts: nil, + weatherChanges: nil, + historicalComparisons: nil ) } diff --git a/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift b/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift index 6cc23c6..13ae738 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/AlertSummary.swift @@ -93,7 +93,7 @@ public enum WeatherResponse: String, Codable, Equatable, Sendable { case assess /// The event no longer poses a threat. - case allClear + case allClear = "all_clear" /// No action recommended. case none diff --git a/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift b/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift index 884591d..3e3d8cd 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/UVIndex.swift @@ -20,7 +20,7 @@ public struct UVIndex: Sendable { /// /// An enumeration that indicates risk of harm from unprotected sun exposure. /// - @frozen public enum ExposureCategory : String, Codable, Comparable, CustomStringConvertible, CaseIterable, Sendable { + @frozen public enum ExposureCategory: String, Codable, Comparable, CustomStringConvertible, CaseIterable, Sendable { /// The UV index is low. /// @@ -40,7 +40,7 @@ public struct UVIndex: Sendable { /// The UV index is very high. /// /// The valid values of this property are 8, 9, and 10. - case veryHigh + case veryHigh = "very_high" /// The UV index is extreme. /// diff --git a/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift b/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift index d31ca3f..5523968 100644 --- a/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift +++ b/Sources/OpenWeatherKit/Public/Characteristics/WeatherCondition.swift @@ -12,75 +12,75 @@ import Foundation public enum WeatherCondition : String, CaseIterable, CustomStringConvertible, Hashable, Sendable { /// The kind of condition. - case blizzard = "Blizzard" + case blizzard = "blizzard" - case blowingDust = "BlowingDust" + case blowingDust = "blowing_dust" - case blowingSnow = "BlowingSnow" + case blowingSnow = "blowing_snow" - case breezy = "Breezy" + case breezy = "breezy" - case clear = "Clear" + case clear = "clear" - case cloudy = "Cloudy" + case cloudy = "cloudy" - case drizzle = "Drizzle" + case drizzle = "drizzle" - case flurries = "Flurries" + case flurries = "flurries" - case foggy = "Foggy" + case foggy = "foggy" - case freezingDrizzle = "FreezingDrizzle" + case freezingDrizzle = "freezing_drizzle" - case freezingRain = "FreezingRain" + case freezingRain = "freezing_rain" - case frigid = "Frigid" + case frigid = "frigid" - case hail = "Hail" + case hail = "hail" - case haze = "Haze" + case haze = "haze" - case heavyRain = "HeavyRain" + case heavyRain = "heavy_rain" - case heavySnow = "HeavySnow" + case heavySnow = "heavy_snow" - case hot = "Hot" + case hot = "hot" - case hurricane = "Hurricane" + case hurricane = "hurricane" - case isolatedThunderstorms = "IsolatedThunderstorms" + case isolatedThunderstorms = "isolated_thunderstorms" - case mostlyClear = "MostlyClear" + case mostlyClear = "mostly_clear" - case mostlyCloudy = "MostlyCloudy" + case mostlyCloudy = "mostly_cloudy" - case partlyCloudy = "PartlyCloudy" + case partlyCloudy = "partly_cloudy" - case rain = "Rain" + case rain = "rain" - case scatteredThunderstorms = "ScatteredThunderstorms" + case scatteredThunderstorms = "scattered_thunderstorms" - case sleet = "Sleet" + case sleet = "sleet" - case smoky = "Smoky" + case smoky = "smoky" - case snow = "Snow" + case snow = "snow" - case strongStorms = "StrongStorms" + case strongStorms = "strong_storms" - case sunFlurries = "SunFlurries" + case sunFlurries = "sun_flurries" - case sunShowers = "SunShowers" + case sunShowers = "sun_showers" - case thunderstorms = "Thunderstorms" + case thunderstorms = "thunderstorms" - case tropicalStorm = "TropicalStorm" + case tropicalStorm = "tropical_storm" - case undefined = "Undefined" + case undefined = "undefined" - case windy = "Windy" + case windy = "windy" - case wintryMix = "WintryMix" + case wintryMix = "wintry_mix" /// Standard string describing the current condition. public var description: String { diff --git a/Sources/OpenWeatherKit/Public/Forecast/HistoricalComparisons.swift b/Sources/OpenWeatherKit/Public/Forecast/HistoricalComparisons.swift new file mode 100644 index 0000000..660db8a --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Forecast/HistoricalComparisons.swift @@ -0,0 +1,122 @@ +// +// HistoricalComparisons.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/31/25. +// + +import Foundation + +/// +/// A structure that represents the weather condition comparisons for a specific location. +/// It's a list of comparisons between current readings and historical averages. +/// The list is ordered by significance of deviation. +/// +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) +public struct HistoricalComparisons: Codable, Equatable, Sendable, RandomAccessCollection { + + /// A type representing the sequence's elements. + public typealias Element = HistoricalComparison + + /// A type that represents a position in the collection. + /// + /// Valid indices consist of the position of every element and a + /// "past the end" position that's not valid for use as a subscript + /// argument. + public typealias Index = Int + + /// A list of comparisons between current readings and historical averages, ordered by significance of deviation. + public var comparisons: [HistoricalComparison] + + /// Descriptive information about the weather comparisons data. + public var metadata: WeatherMetadata + + /// The start index for the historical comparisons. + public var startIndex: HistoricalComparisons.Index { comparisons.startIndex } + + /// The end index for the historical comparisons. + public var endIndex: HistoricalComparisons.Index { comparisons.endIndex } + + /// The historical comparison at the provided index. + public subscript(position: HistoricalComparisons.Index) -> HistoricalComparisons.Element { comparisons[position] } +} + +/// +/// An enum that represents a recognized comparison in the statistical analysis of a location's historical weather data. +/// +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) +public enum HistoricalComparison: Codable, Equatable, Sendable { + + /// The comparison relates to the location's maximum temperature averaged since ~1970. + case highTemperature(Trend) + + /// The comparison relates to the location's minimum temperature averaged since ~1970. + case lowTemperature(Trend) + + /// The comparison relates to the amount of precipitation at the location averaged over the past 30 days. + case precipitationAmount(Trend) + + /// The comparison relates to the amount of snowfall at the location averaged over the past 30 days. + case snowfallAmount(Trend) +} + +/// +/// A structure describing an observed pattern in the data for weather at a location for a specific condition. +/// +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) +public struct Trend: Codable, Sendable, Equatable where Dimension: Unit { + + /// The manner in which the comparison between the baseline and current values are compared. + public var baseline: TrendBaseline + + /// The current recorded value for the condition in which the trend is compared against. + public var currentValue: Measurement + + /// Semantically describes the manner in which the observed trend compares the current value against the baseline value. + public var deviation: Deviation +} + +/// +/// A type encapsulating everything there is to know about what a trend baseline is. +/// +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) +public struct TrendBaseline : Codable, Sendable, Equatable where Dimension: Unit { + + /// An enum describing what value is being compared between historical and current readings. + public enum Kind: String, Codable, Equatable, Sendable, Hashable { + + /// The baseline value is a mean (or average) of other values. + case mean + } + + /// The manner in which the comparison between the baseline and current values are compared. + public let kind: TrendBaseline.Kind + + /// The recorded baseline value for the condition in which the trend is comparing to. + public let value: Measurement + + /// The year the statistics collection began. + public let startDate: Date +} + +/// +/// Describes a comparison between two values in a trend. +/// +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) +public enum Deviation: String, Codable, Equatable, Sendable, Hashable { + + /// The most recently observed value is much larger than the value it is being compared against. + case muchHigher = "much_higher" + + /// The most recently observed value is larger than the value it is being compared against. + case higher + + /// The most recently observed value is about the same as the value it is being compared against. + case normal + + /// The most recently observed value is lower than the value it is being compared against. + case lower + + /// The most recently observed value is much lower than the value it is being compared against. + case muchLower = "much_lower" +} diff --git a/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift b/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift index fc05167..312b24e 100644 --- a/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift +++ b/Sources/OpenWeatherKit/Public/Forecast/WeatherAvailability.swift @@ -18,13 +18,13 @@ public struct WeatherAvailability: Sendable { public var alertAvailability: WeatherAvailability.AvailabilityKind /// The availability kind. - public enum AvailabilityKind : String, Codable, Sendable { + public enum AvailabilityKind: String, Codable, Sendable { /// The data is available. case available /// The data is supported for the location but is temporarily unavailable. - case temporarilyUnavailable + case temporarilyUnavailable = "temporarily_unavailable" /// The data isn't supported for the location. case unsupported diff --git a/Sources/OpenWeatherKit/Public/Forecast/WeatherChanges.swift b/Sources/OpenWeatherKit/Public/Forecast/WeatherChanges.swift new file mode 100644 index 0000000..e60e907 --- /dev/null +++ b/Sources/OpenWeatherKit/Public/Forecast/WeatherChanges.swift @@ -0,0 +1,74 @@ +// +// WeatherChanges.swift +// open-weather-kit +// +// Created by Jeremy Greenwood on 10/31/25. +// + +import Foundation + +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) +public struct WeatherChanges: RandomAccessCollection, Sendable, Codable, Equatable { + + /// A type representing the sequence's elements. + public typealias Element = WeatherChange + + /// A type that represents a position in the collection. + /// + /// Valid indices consist of the position of every element and a + /// "past the end" position that's not valid for use as a subscript + /// argument. + public typealias Index = Int + + /// A list of forecasted weather changes, in chronological order. + public var changes: [WeatherChange] + + /// Descriptive information about the weather change data. + public var metadata: WeatherMetadata + + /// The start index for the weather changes. + public var startIndex: WeatherChanges.Index { changes.startIndex } + + /// The end index for the weather changes. + public var endIndex: WeatherChanges.Index { changes.endIndex } + + /// The weather change at the provided index. + public subscript(position: WeatherChanges.Index) -> WeatherChanges.Element { changes[position] } +} + +/// +/// A structure that informs how certain measurable weather aspects are expected to change relative to before. +/// +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) +public struct WeatherChange: Sendable, Codable, Equatable { + + /// + /// An enum that specifies the direction in which a measurable aspect of the weather is expected to change. + /// + @frozen public enum Direction: String, Codable, Sendable, Equatable { + + /// The value will be significantly higher than before. + case increase + + /// The value will be significantly lower than before. + case decrease + + /// The value will remain similar to before. + case steady + } + + /// The date at which this change record becomes effective. + public var date: Date + + /// How the high temperature for this day compares to that of before. + public var highTemperature: WeatherChange.Direction + + /// How the low temperature for this day compares to that of before. + public var lowTemperature: WeatherChange.Direction + + /// How the forecasted precipitation amount for this day, during daylight hours, compares to that of before. + public var dayPrecipitationAmount: WeatherChange.Direction + + /// How the forecasted precipitation amount, during the night of this day, compares to that of before. + public var nightPrecipitationAmount: WeatherChange.Direction +} diff --git a/Sources/OpenWeatherKit/Public/Requests/DailyWeatherStatisticsQuery.swift b/Sources/OpenWeatherKit/Public/Requests/DailyWeatherStatisticsQuery.swift index 89467d3..7985717 100644 --- a/Sources/OpenWeatherKit/Public/Requests/DailyWeatherStatisticsQuery.swift +++ b/Sources/OpenWeatherKit/Public/Requests/DailyWeatherStatisticsQuery.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates a daily weather statistics dataset request. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct DailyWeatherStatisticsQuery: Sendable where T: Decodable, T: Encodable, T: Equatable, T: Sendable { @usableFromInline internal let statisticsType: StatisticsType diff --git a/Sources/OpenWeatherKit/Public/Requests/DailyWeatherSummaryQuery.swift b/Sources/OpenWeatherKit/Public/Requests/DailyWeatherSummaryQuery.swift index a650c1a..60166ca 100644 --- a/Sources/OpenWeatherKit/Public/Requests/DailyWeatherSummaryQuery.swift +++ b/Sources/OpenWeatherKit/Public/Requests/DailyWeatherSummaryQuery.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates a daily weather summary dataset request. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct DailyWeatherSummaryQuery: Sendable where T: Decodable, T: Encodable, T: Equatable, T: Sendable { @usableFromInline internal let statisticsType: StatisticsType diff --git a/Sources/OpenWeatherKit/Public/Requests/HourlyWeatherStatisticsQuery.swift b/Sources/OpenWeatherKit/Public/Requests/HourlyWeatherStatisticsQuery.swift index ed1bf28..1a75505 100644 --- a/Sources/OpenWeatherKit/Public/Requests/HourlyWeatherStatisticsQuery.swift +++ b/Sources/OpenWeatherKit/Public/Requests/HourlyWeatherStatisticsQuery.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates an hourly weather statistics dataset request. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct HourlyWeatherStatisticsQuery: Sendable where T: Decodable, T: Encodable, T: Equatable, T: Sendable { @usableFromInline internal let statisticsType: StatisticsType diff --git a/Sources/OpenWeatherKit/Public/Requests/MonthlyWeatherStatisticsQuery.swift b/Sources/OpenWeatherKit/Public/Requests/MonthlyWeatherStatisticsQuery.swift index e6ae4b1..6071ea8 100644 --- a/Sources/OpenWeatherKit/Public/Requests/MonthlyWeatherStatisticsQuery.swift +++ b/Sources/OpenWeatherKit/Public/Requests/MonthlyWeatherStatisticsQuery.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates a monthly weather statistics dataset request. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct MonthlyWeatherStatisticsQuery: Sendable where T: Decodable, T: Encodable, T: Equatable, T: Sendable { @usableFromInline internal let statisticsType: StatisticsType diff --git a/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift b/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift index d537b03..7af3c68 100644 --- a/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift +++ b/Sources/OpenWeatherKit/Public/Requests/WeatherQuery.swift @@ -20,7 +20,8 @@ public struct WeatherQuery { public static var current: WeatherQuery { WeatherQuery( queryType: .current(APIWeather.CodingKeys.currentWeather.rawValue), - result: { try $0.currentWeather + result: { + try $0.currentWeather .unwrap(or: WeatherError.missingData(APIWeather.CodingKeys.currentWeather.rawValue)) } ) @@ -30,7 +31,8 @@ public struct WeatherQuery { public static var minute: WeatherQuery?> { WeatherQuery?>( queryType: .minute(APIWeather.CodingKeys.forecastNextHour.rawValue), - result: { try $0.minuteForecast + result: { + try $0.minuteForecast .unwrap(or: WeatherError.missingData(APIWeather.CodingKeys.forecastNextHour.rawValue)) } ) @@ -44,7 +46,8 @@ public struct WeatherQuery { Date(), Date.hoursFromNow(24) ), - result: { try $0.hourlyForecast + result: { + try $0.hourlyForecast .unwrap(or: WeatherError.missingData(APIWeather.CodingKeys.forecastHourly.rawValue)) } ) @@ -58,12 +61,37 @@ public struct WeatherQuery { Date(), Date.daysFromNow(10) ), - result: { try $0.dailyForecast + result: { + try $0.dailyForecast .unwrap(or: WeatherError.missingData(APIWeather.CodingKeys.forecastDaily.rawValue)) } ) } + /// The weather changes query. + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) + public static var changes: WeatherQuery { + WeatherQuery( + queryType: .changes(APIWeather.CodingKeys.weatherChanges.rawValue), + result: { + try $0.weatherChanges + .unwrap(or: WeatherError.missingData(APIWeather.CodingKeys.weatherChanges.rawValue)) + } + ) + } + + /// The weather historical comparison query. + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) + public static var historicalComparisons: WeatherQuery { + WeatherQuery( + queryType: .comparisons(APIWeather.CodingKeys.historicalComparisons.rawValue), + result: { + try $0.historicalComparisons + .unwrap(or: WeatherError.missingData(APIWeather.CodingKeys.historicalComparisons.rawValue)) + } + ) + } + #if canImport(CoreLocation) public static var alerts: WeatherQuery<[WeatherAlert]?> { WeatherQuery<[WeatherAlert]?>( diff --git a/Sources/OpenWeatherKit/Public/Statistics/DailyWeatherStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/DailyWeatherStatistics.swift index 7e8b96d..e701fdd 100644 --- a/Sources/OpenWeatherKit/Public/Statistics/DailyWeatherStatistics.swift +++ b/Sources/OpenWeatherKit/Public/Statistics/DailyWeatherStatistics.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that contains daily climatological statistics for a location. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct DailyWeatherStatistics: Codable, Equatable, Sendable, RandomAccessCollection where T: Decodable, T: Encodable, T: Equatable, T: Sendable { /// A type representing the sequence's elements. diff --git a/Sources/OpenWeatherKit/Public/Statistics/DayPrecipitationStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/DayPrecipitationStatistics.swift index 2afb134..8fb4aa4 100644 --- a/Sources/OpenWeatherKit/Public/Statistics/DayPrecipitationStatistics.swift +++ b/Sources/OpenWeatherKit/Public/Statistics/DayPrecipitationStatistics.swift @@ -8,7 +8,7 @@ import Foundation /// Precipitation statistics for a specific day of the year. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct DayPrecipitationStatistics: Codable, Equatable, Sendable { /// The day of the year, in UTC. /// diff --git a/Sources/OpenWeatherKit/Public/Statistics/DayTemperatureStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/DayTemperatureStatistics.swift index 1e9326d..0b466e3 100644 --- a/Sources/OpenWeatherKit/Public/Statistics/DayTemperatureStatistics.swift +++ b/Sources/OpenWeatherKit/Public/Statistics/DayTemperatureStatistics.swift @@ -8,7 +8,7 @@ import Foundation /// Temperature statistics for a specific day of the year. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct DayTemperatureStatistics: Codable, Equatable, Sendable { /// The day of the year, in UTC. /// diff --git a/Sources/OpenWeatherKit/Public/Statistics/HourTemperatureStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/HourTemperatureStatistics.swift index 74391d5..9b0dc9c 100644 --- a/Sources/OpenWeatherKit/Public/Statistics/HourTemperatureStatistics.swift +++ b/Sources/OpenWeatherKit/Public/Statistics/HourTemperatureStatistics.swift @@ -8,7 +8,7 @@ import Foundation /// Temperature statistics for a specific hour of the year. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct HourTemperatureStatistics: Codable, Equatable, Sendable { /// The hour of the year, in UTC. /// diff --git a/Sources/OpenWeatherKit/Public/Statistics/HourlyWeatherStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/HourlyWeatherStatistics.swift index 03b63c5..9ad43d4 100644 --- a/Sources/OpenWeatherKit/Public/Statistics/HourlyWeatherStatistics.swift +++ b/Sources/OpenWeatherKit/Public/Statistics/HourlyWeatherStatistics.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that contains hourly climatological statistics for a location. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct HourlyWeatherStatistics: Codable, Equatable, Sendable, RandomAccessCollection where T: Decodable, T: Encodable, T: Equatable, T: Sendable { /// A type representing the sequence's elements. diff --git a/Sources/OpenWeatherKit/Public/Statistics/MonthPrecipitationStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/MonthPrecipitationStatistics.swift index a90810f..8a2884a 100644 --- a/Sources/OpenWeatherKit/Public/Statistics/MonthPrecipitationStatistics.swift +++ b/Sources/OpenWeatherKit/Public/Statistics/MonthPrecipitationStatistics.swift @@ -8,7 +8,7 @@ import Foundation /// Precipitation statistics for a specific month. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct MonthPrecipitationStatistics: Codable, Equatable, Sendable { /// The month of the year, in UTC. /// diff --git a/Sources/OpenWeatherKit/Public/Statistics/MonthTemperatureStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/MonthTemperatureStatistics.swift index 75bc6fa..73babfa 100644 --- a/Sources/OpenWeatherKit/Public/Statistics/MonthTemperatureStatistics.swift +++ b/Sources/OpenWeatherKit/Public/Statistics/MonthTemperatureStatistics.swift @@ -8,7 +8,7 @@ import Foundation /// Temperature statistics for a specific month. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct MonthTemperatureStatistics: Codable, Equatable, Sendable { /// The month of the year, in UTC. diff --git a/Sources/OpenWeatherKit/Public/Statistics/MonthlyWeatherStatistics.swift b/Sources/OpenWeatherKit/Public/Statistics/MonthlyWeatherStatistics.swift index 02974a1..dffe792 100644 --- a/Sources/OpenWeatherKit/Public/Statistics/MonthlyWeatherStatistics.swift +++ b/Sources/OpenWeatherKit/Public/Statistics/MonthlyWeatherStatistics.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that contains monthly climatological statistics for a location. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct MonthlyWeatherStatistics: Codable, Equatable, Sendable, RandomAccessCollection where T: Decodable, T: Encodable, T: Equatable, T: Sendable { /// A type representing the sequence's elements. diff --git a/Sources/OpenWeatherKit/Public/Statistics/Percentiles.swift b/Sources/OpenWeatherKit/Public/Statistics/Percentiles.swift index a322e98..60654e4 100644 --- a/Sources/OpenWeatherKit/Public/Statistics/Percentiles.swift +++ b/Sources/OpenWeatherKit/Public/Statistics/Percentiles.swift @@ -10,7 +10,7 @@ import Foundation /// /// A structure that describes probability distributions for a measurable weather condition. /// -@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct Percentiles : Codable, Equatable, Sendable where Dimension : Unit { /// 10% of the distribution is less than this value. diff --git a/Sources/OpenWeatherKit/Public/Summary/DailyWeatherSummary.swift b/Sources/OpenWeatherKit/Public/Summary/DailyWeatherSummary.swift index cec80f6..4ac8b6f 100644 --- a/Sources/OpenWeatherKit/Public/Summary/DailyWeatherSummary.swift +++ b/Sources/OpenWeatherKit/Public/Summary/DailyWeatherSummary.swift @@ -10,7 +10,7 @@ import Foundation /// /// A structure that holds a collection of day weather summaries. /// -@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct DailyWeatherSummary : Codable, Equatable, Sendable, RandomAccessCollection where T : Decodable, T : Encodable, T : Equatable, T : Sendable { /// A type representing the sequence's elements. diff --git a/Sources/OpenWeatherKit/Public/Summary/DayPrecipitationSummary.swift b/Sources/OpenWeatherKit/Public/Summary/DayPrecipitationSummary.swift index 7301e0f..48a8754 100644 --- a/Sources/OpenWeatherKit/Public/Summary/DayPrecipitationSummary.swift +++ b/Sources/OpenWeatherKit/Public/Summary/DayPrecipitationSummary.swift @@ -10,7 +10,7 @@ import Foundation /// /// A structure that describes the precipitation summary for a day. /// -@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct DayPrecipitationSummary : Codable, Equatable, Sendable { /// The day of the observed precipitation summary diff --git a/Sources/OpenWeatherKit/Public/Summary/DayTemperatureSummary.swift b/Sources/OpenWeatherKit/Public/Summary/DayTemperatureSummary.swift index b9f70a0..25f985e 100644 --- a/Sources/OpenWeatherKit/Public/Summary/DayTemperatureSummary.swift +++ b/Sources/OpenWeatherKit/Public/Summary/DayTemperatureSummary.swift @@ -10,7 +10,7 @@ import Foundation /// /// A structure that describes the temperature summary for a day. /// -@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +@available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) public struct DayTemperatureSummary : Codable, Equatable, Sendable { /// The day of the observed temperature summary. diff --git a/Sources/OpenWeatherKit/Public/WeatherService+Forecast.swift b/Sources/OpenWeatherKit/Public/WeatherService+Forecast.swift index f8a9296..2462644 100644 --- a/Sources/OpenWeatherKit/Public/WeatherService+Forecast.swift +++ b/Sources/OpenWeatherKit/Public/WeatherService+Forecast.swift @@ -21,7 +21,11 @@ extension WeatherService { @inlinable final public func weather(for location: LocationProtocol) async throws -> Weather { let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await getWeather(location: location, countryCode: countryCode, timezone: timezone) + return try await getWeather( + location: location, + countryCode: countryCode, + timezone: timezone + ) } #endif @@ -38,7 +42,12 @@ extension WeatherService { timezone: TimeZone, language: WeatherService.Configuration.Language? = nil ) async throws -> Weather { - try await getWeather(location: location, countryCode: countryCode, timezone: timezone, language: language) + try await getWeather( + location: location, + countryCode: countryCode, + timezone: timezone, + language: language + ) } /// @@ -61,7 +70,12 @@ extension WeatherService { including dataSet: WeatherQuery ) async throws -> T { let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet, countryCode: countryCode, timezone: timezone) + return try await weather( + for: location, + including: dataSet, + countryCode: countryCode, + timezone: timezone + ) } #endif @@ -77,7 +91,7 @@ extension WeatherService { let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, - queries: _dataSet, + queries: [_dataSet], timezone: timezone, jwt: self.configuration.jwt() ) @@ -106,7 +120,12 @@ extension WeatherService { _ dataSet2: WeatherQuery ) async throws -> (T1, T2) { let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet1, dataSet2, countryCode: countryCode, timezone: timezone) + return try await weather( + for: location, + including: dataSet1, dataSet2, + countryCode: countryCode, + timezone: timezone + ) } #endif @@ -124,7 +143,7 @@ extension WeatherService { let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, - queries: _dataSet1, _dataSet2, + queries: [_dataSet1, _dataSet2], timezone: timezone, jwt: self.configuration.jwt() ) @@ -154,7 +173,12 @@ extension WeatherService { _ dataSet3: WeatherQuery ) async throws -> (T1, T2, T3) { let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, countryCode: countryCode, timezone: timezone) + return try await weather( + for: location, + including: dataSet1, dataSet2, dataSet3, + countryCode: countryCode, + timezone: timezone + ) } #endif @@ -174,7 +198,7 @@ extension WeatherService { let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, - queries: _dataSet1, _dataSet2, _dataSet3, + queries: [_dataSet1, _dataSet2, _dataSet3], timezone: timezone, jwt: self.configuration.jwt() ) @@ -206,7 +230,12 @@ extension WeatherService { _ dataSet4: WeatherQuery ) async throws -> (T1, T2, T3, T4) { let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, countryCode: countryCode, timezone: timezone) + return try await weather( + for: location, + including: dataSet1, dataSet2, dataSet3, dataSet4, + countryCode: countryCode, + timezone: timezone + ) } #endif @@ -228,7 +257,7 @@ extension WeatherService { let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, - queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, + queries: [_dataSet1, _dataSet2, _dataSet3, _dataSet4], timezone: timezone, jwt: self.configuration.jwt() ) @@ -262,7 +291,12 @@ extension WeatherService { _ dataSet5: WeatherQuery ) async throws -> (T1, T2, T3, T4, T5) { let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, dataSet5, countryCode: countryCode, timezone: timezone) + return try await weather( + for: location, + including: dataSet1, dataSet2, dataSet3, dataSet4, dataSet5, + countryCode: countryCode, + timezone: timezone + ) } #endif @@ -286,7 +320,7 @@ extension WeatherService { let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, - queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, _dataSet5, + queries: [_dataSet1, _dataSet2, _dataSet3, _dataSet4, _dataSet5], timezone: timezone, jwt: self.configuration.jwt() ) @@ -322,7 +356,12 @@ extension WeatherService { _ dataSet6: WeatherQuery ) async throws -> (T1, T2, T3, T4, T5, T6) { let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) - return try await weather(for: location, including: dataSet1, dataSet2, dataSet3, dataSet4, dataSet5, dataSet6, countryCode: countryCode, timezone: timezone) + return try await weather( + for: location, + including: dataSet1, dataSet2, dataSet3, dataSet4, dataSet5, dataSet6, + countryCode: countryCode, + timezone: timezone + ) } #endif @@ -348,7 +387,7 @@ extension WeatherService { let proxy = try await networkClient.fetchWeather( location: location, language: self.configuration.language, - queries: _dataSet1, _dataSet2, _dataSet3, _dataSet4, _dataSet5, _dataSet6, + queries: [_dataSet1, _dataSet2, _dataSet3, _dataSet4, _dataSet5, _dataSet6], timezone: timezone, jwt: self.configuration.jwt() ) @@ -362,6 +401,47 @@ extension WeatherService { _dataSet6.result(proxy) ) } + + /// + /// Returns the weather forecast for the requested location. + /// + /// - Parameter location: The requested location. + /// - Throws: Weather data error `WeatherError` + /// - Returns: The requested weather data set. + /// + /// This is a variadic API in which any combination of data sets can be requested and returned as a tuple. Here's an example: + /// + /// ``` + /// `let (current, minute, hourly, daily, alerts) = try await service.weather(for: newYork, including: .current, .minute, .hourly, .daily, .alerts)` + /// ``` +#if canImport(CoreLocation) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) + @preconcurrency final public func weather( + for location: CLLocation, + including dataSets: repeat WeatherQuery + ) async throws -> (repeat each T) { + let (countryCode, timezone) = try await resolveCountryCodeAndTimezone(for: location) + + // Update each dataSet with countryCode + let _dataSets = (repeat (each dataSets).update(with: countryCode)) + + // The network client's fetchWeather method expects an array, so convert the pack into an array + var queries: [any Query] = [] + repeat queries.append((each _dataSets)) // Pack expansion into the array + + // Perform the network fetch with all queries batched together + let proxy = try await networkClient.fetchWeather( + location: location, + language: self.configuration.language, + queries: queries, + timezone: timezone, + jwt: self.configuration.jwt() + ) + + // Expand the pack to extract the typed results from the proxy for each query + return (repeat try (each dataSets).result(proxy)) + } +#endif } extension WeatherService { @@ -380,16 +460,23 @@ extension WeatherService { #endif @usableFromInline - func getWeather(location: LocationProtocol, countryCode: String, timezone: TimeZone, language: WeatherService.Configuration.Language? = nil) async throws -> Weather { + func getWeather( + location: LocationProtocol, + countryCode: String, + timezone: TimeZone, + language: WeatherService.Configuration.Language? = nil + ) async throws -> Weather { let proxy = try await networkClient.fetchWeather( location: location, language: language ?? self.configuration.language, - queries: WeatherQuery.current, - WeatherQuery?>.minute, - WeatherQuery>.hourly, - WeatherQuery>.daily, - WeatherQuery<[WeatherAlert]?>.alerts(countryCode: countryCode), - WeatherQuery.availability(countryCode: countryCode), + queries: [ + WeatherQuery.current, + WeatherQuery?>.minute, + WeatherQuery>.hourly, + WeatherQuery>.daily, + WeatherQuery<[WeatherAlert]?>.alerts(countryCode: countryCode), + WeatherQuery.availability(countryCode: countryCode) + ], timezone: timezone, jwt: self.configuration.jwt() ) @@ -412,3 +499,4 @@ extension WeatherService { ) } } + diff --git a/Sources/OpenWeatherKit/Public/WeatherService+Statistics.swift b/Sources/OpenWeatherKit/Public/WeatherService+Statistics.swift index 4e22f18..2426ecc 100644 --- a/Sources/OpenWeatherKit/Public/WeatherService+Statistics.swift +++ b/Sources/OpenWeatherKit/Public/WeatherService+Statistics.swift @@ -34,7 +34,7 @@ extension WeatherService { /// ``` /// /// - Precondition: `startDay in 1...366 && endDay in 1...366` - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func dailyStatistics( for location: LocationProtocol, @@ -86,7 +86,7 @@ extension WeatherService { /// let (dailyPrecipitationStatistics, dailyTemperatureStatistics) = try await service.dailyStatistics(for: newYork, forDaysIn: timeInterval, including: .precipitation, .temperature) /// ``` /// - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func dailyStatistics( for location: LocationProtocol, @@ -138,7 +138,7 @@ extension WeatherService { /// let (dailyPrecipitationStatistics, dailyTemperatureStatistics) = try await service.dailyStatistics(for: newYork, including: .precipitation, .temperature) /// ``` /// - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func dailyStatistics( for location: LocationProtocol, @@ -183,7 +183,7 @@ extension WeatherService { /// ``` /// /// - Precondition: `startHour in 1...8784 && endHour in 1...8784` - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func hourlyStatistics( for location: LocationProtocol, @@ -224,7 +224,7 @@ extension WeatherService { /// let hourlyTemperatureStatistics = try await service.hourlyStatistics(for: newYork, forHoursIn: interval, including: .temperature) /// ``` /// - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func hourlyStatistics( for location: LocationProtocol, @@ -262,7 +262,7 @@ extension WeatherService { /// let hourlyTemperatureStatistics = try await service.hourlyStatistics(for: newYork, including: .temperature) /// ``` /// - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func hourlyStatistics( for location: LocationProtocol, @@ -307,7 +307,7 @@ extension WeatherService { /// ``` /// /// - Precondition: `startMonth in 1...12 && endMonth in 1...12` - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func monthlyStatistics( for location: LocationProtocol, @@ -348,7 +348,7 @@ extension WeatherService { /// let (monthlyPrecipitationStatistics, monthlyTemperatureStatistics) = try await service.monthlyStatistics(for: newYork, forMonthsIn: interval, including: .precipitation, .temperature) /// ``` /// - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func monthlyStatistics( for location: LocationProtocol, @@ -384,7 +384,7 @@ extension WeatherService { /// let (monthlyPrecipitationStatistics, monthlyTemperatureStatistics) = try await service.monthlyStatistics(for: newYork, including: .precipitation, .temperature) /// ``` /// - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func monthlyStatistics( for location: LocationProtocol, diff --git a/Sources/OpenWeatherKit/Public/WeatherService+Summary.swift b/Sources/OpenWeatherKit/Public/WeatherService+Summary.swift index c8cd852..ff2ef1b 100644 --- a/Sources/OpenWeatherKit/Public/WeatherService+Summary.swift +++ b/Sources/OpenWeatherKit/Public/WeatherService+Summary.swift @@ -35,7 +35,7 @@ extension WeatherService { /// ``` /// /// - Precondition: `startDay in 1...366 && endDay in 1...366` - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func dailySummary( for location: LocationProtocol, @@ -85,7 +85,7 @@ extension WeatherService { /// let (dailyPrecipitationSummary, dailyTemperatureSummary) = try await service.dailySummary(for: newYork, forDaysIn: timeInterval, including: .precipitation, .temperature) /// ``` /// - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func dailySummary( for location: LocationProtocol, @@ -133,7 +133,7 @@ extension WeatherService { /// let (dailyPrecipitationSummary, dailyTemperatureSummary) = try await service.dailySummary(for: newYork, including: .precipitation, .temperature) /// ``` /// - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(macOS 11, iOS 13, watchOS 6, tvOS 13, visionOS 1, *) @inlinable final public func dailySummary( for location: LocationProtocol, diff --git a/Tests/OpenWeatherKitTests/Utils/MockClient.swift b/Tests/OpenWeatherKitTests/Utils/MockClient.swift index ca16d22..95edaec 100644 --- a/Tests/OpenWeatherKitTests/Utils/MockClient.swift +++ b/Tests/OpenWeatherKitTests/Utils/MockClient.swift @@ -74,7 +74,9 @@ actor MockClient: Client { forecastDaily: include.contains(.daily) ? MockData.dailyWeather : nil, forecastHourly: include.contains(.hourly) ? MockData.hourlyWeather : nil, forecastNextHour: include.contains(.nextHour) ? MockData.nextHourWeather : nil, - weatherAlerts: include.contains(.alerts) ? MockData.alerts : nil + historicalComparisons: include.contains(.nextHour) ? MockData.historicalComparisons : nil, + weatherAlerts: include.contains(.alerts) ? MockData.alerts : nil, + weatherChanges: include.contains(.nextHour) ? MockData.changes : nil ) } } diff --git a/Tests/OpenWeatherKitTests/Utils/MockData.swift b/Tests/OpenWeatherKitTests/Utils/MockData.swift index 43bbe89..58d2b8a 100644 --- a/Tests/OpenWeatherKitTests/Utils/MockData.swift +++ b/Tests/OpenWeatherKitTests/Utils/MockData.swift @@ -21,6 +21,14 @@ struct MockData { decode(APIForecastDaily.self, from: dailyWeatherJSON) } + static var changes: APIWeatherChanges { + decode(APIWeatherChanges.self, from: changesJSON) + } + + static var historicalComparisons: APIHistoricalComparisons { + decode(APIHistoricalComparisons.self, from: historicalComparisonJSON) + } + static var hourlyWeather: APIForecastHourly { decode(APIForecastHourly.self, from: hourlyWeatherJSON) } @@ -57,6 +65,14 @@ fileprivate extension MockData { {"metadata":{"latitude":40.709999,"longitude":-74.010002,"attributionUrl":"https://developer.apple.com/weatherkit/data-source-attribution/","readTime":1760992827,"expireTime":1760996295,"temporarilyUnavailable":false,"sourceType":"MODELED","reportedTime":1760961934},"days":[{"moonrise":1760955677,"moonset":1760995956,"sunrise":1760958786,"sunset":1760998079,"forecastEnd":1761019200,"sunriseCivil":1760957122,"snowfallAmount":0.0,"temperatureMax":19.604397,"solarMidnight":1760935261,"windSpeedAvg":20.323332,"temperatureMaxTime":1760940462,"windGustSpeedMax":56.481377,"precipitationAmount":1.426,"sunriseAstronomical":1760953309,"forecastStart":1760932800,"moonPhase":"WANING_CRESCENT","humidityMax":88,"precipitationAmountByType":[{"expected":1.426,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"sunsetAstronomical":1761003557,"visibilityMax":34655.355469,"precipitationChance":80,"temperatureMin":12.16,"maxUvIndex":3,"temperatureMinTime":1761019200,"sunsetNautical":1761001653,"daytimeForecast":{"humidity":68,"daylight":true,"forecastEnd":1761001200,"snowfallAmount":0.0,"temperatureMax":16.590218,"uvIndexMin":0,"temperatureApparentMin":8.725781,"windGustSpeedMax":56.481377,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":13.272897,"precipitationAmount":0.0,"forecastStart":1760958000,"windDirection":259,"windSpeed":24.186375,"humidityMax":87,"precipitationAmountByType":[],"cloudCoverLowAltPct":60,"visibilityMax":34655.355469,"cloudCover":67,"precipitationChance":0,"temperatureMin":14.034677,"cloudCoverMidAltPct":31,"precipitationIntensityMax":0.0,"conditionCode":"WINDY","cloudCoverHighAltPct":0,"humidityMin":58,"visibilityMin":17583.0,"precipitationType":"CLEAR","windSpeedMax":28.665266},"conditionCode":"RAIN","humidityMin":58,"solarNoon":1760978432,"visibilityMin":12018.388672,"overnightForecast":{"humidity":68,"daylight":false,"forecastEnd":1761044400,"snowfallAmount":0.0,"temperatureMax":15.22,"uvIndexMin":0,"temperatureApparentMin":5.715589,"windGustSpeedMax":38.051998,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":10.461295,"precipitationAmount":0.0,"forecastStart":1761001200,"windDirection":261,"windSpeed":14.514926,"humidityMax":76,"precipitationAmountByType":[],"cloudCoverLowAltPct":3,"visibilityMax":34123.0,"cloudCover":4,"precipitationChance":0,"temperatureMin":9.48,"cloudCoverMidAltPct":0,"precipitationIntensityMax":0.0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"humidityMin":60,"visibilityMin":31188.0,"precipitationType":"CLEAR","windSpeedMax":19.027884},"precipitationType":"RAIN","sunriseNautical":1760955207,"sunsetCivil":1760999748,"windSpeedMax":28.665266,"restOfDayForecast":{"humidity":62,"daylight":false,"forecastEnd":1761019200,"snowfallAmount":0.0,"temperatureMax":16.498795,"uvIndexMin":0,"temperatureApparentMin":7.687512,"windGustSpeedMax":48.869514,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":11.805491,"precipitationAmount":0.0,"forecastStart":1760992827,"windDirection":269,"windSpeed":18.297956,"humidityMax":67,"precipitationAmountByType":[],"cloudCoverLowAltPct":13,"visibilityMax":34655.355469,"cloudCover":19,"precipitationChance":0,"temperatureMin":12.16,"cloudCoverMidAltPct":7,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"humidityMin":58,"visibilityMin":32719.0,"precipitationType":"CLEAR","windSpeedMax":24.704273}},{"moonrise":1761045789,"moonset":1761083664,"sunrise":1761045253,"sunset":1761084393,"forecastEnd":1761105600,"sunriseCivil":1761043586,"snowfallAmount":0.0,"temperatureMax":19.11182,"solarMidnight":1761021651,"windSpeedAvg":13.211013,"temperatureMaxTime":1761076535,"windGustSpeedMax":33.994553,"precipitationAmount":1.693,"sunriseAstronomical":1761039771,"forecastStart":1761019200,"moonPhase":"NEW","humidityMax":78,"precipitationAmountByType":[{"expected":1.693,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"sunsetAstronomical":1761089876,"visibilityMax":33198.886719,"precipitationChance":13,"temperatureMin":9.479772,"maxUvIndex":4,"temperatureMinTime":1761044666,"sunsetNautical":1761087972,"daytimeForecast":{"humidity":59,"daylight":true,"forecastEnd":1761087600,"snowfallAmount":0.0,"temperatureMax":19.11182,"uvIndexMin":0,"temperatureApparentMin":5.913912,"windGustSpeedMax":33.994553,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":4,"temperatureApparentMax":18.34053,"precipitationAmount":0.0,"forecastStart":1761044400,"windDirection":203,"windSpeed":13.429988,"humidityMax":76,"precipitationAmountByType":[],"cloudCoverLowAltPct":0,"visibilityMax":33198.886719,"cloudCover":5,"precipitationChance":0,"temperatureMin":9.479772,"cloudCoverMidAltPct":3,"precipitationIntensityMax":0.0,"conditionCode":"CLEAR","cloudCoverHighAltPct":6,"humidityMin":48,"visibilityMin":29599.0,"precipitationType":"CLEAR","windSpeedMax":16.954836},"conditionCode":"CLEAR","humidityMin":48,"solarNoon":1761064823,"visibilityMin":23130.0,"overnightForecast":{"humidity":80,"daylight":false,"forecastEnd":1761130800,"snowfallAmount":0.0,"temperatureMax":17.129999,"uvIndexMin":0,"temperatureApparentMin":10.810381,"windGustSpeedMax":33.079964,"perceivedPrecipitationIntensityMax":1.29,"uvIndexMax":0,"temperatureApparentMax":14.194607,"precipitationAmount":4.369,"forecastStart":1761087600,"windDirection":199,"windSpeed":10.360663,"humidityMax":91,"precipitationAmountByType":[{"expected":4.368999,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"cloudCoverLowAltPct":26,"visibilityMax":31297.451172,"cloudCover":69,"precipitationChance":38,"temperatureMin":12.25,"cloudCoverMidAltPct":58,"precipitationIntensityMax":1.694435,"conditionCode":"RAIN","cloudCoverHighAltPct":13,"humidityMin":63,"visibilityMin":14850.519531,"precipitationType":"RAIN","windSpeedMax":14.436001},"precipitationType":"RAIN","sunriseNautical":1761041670,"sunsetCivil":1761086065,"windSpeedMax":16.954836},{"moonrise":1761135949,"moonset":1761171569,"sunrise":1761131720,"sunset":1761170708,"forecastEnd":1761192000,"sunriseCivil":1761130050,"snowfallAmount":0.0,"temperatureMax":16.530001,"solarMidnight":1761108041,"windSpeedAvg":14.159187,"temperatureMaxTime":1761156000,"windGustSpeedMax":41.528694,"precipitationAmount":2.676,"sunriseAstronomical":1761126232,"forecastStart":1761105600,"moonPhase":"WAXING_CRESCENT","humidityMax":91,"precipitationAmountByType":[{"expected":2.676,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"sunsetAstronomical":1761176196,"visibilityMax":32597.847656,"precipitationChance":47,"temperatureMin":10.43,"maxUvIndex":3,"temperatureMinTime":1761192000,"sunsetNautical":1761174291,"daytimeForecast":{"humidity":62,"daylight":true,"forecastEnd":1761174000,"snowfallAmount":0.0,"temperatureMax":16.530001,"uvIndexMin":0,"temperatureApparentMin":8.525494,"windGustSpeedMax":41.528694,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":13.53885,"precipitationAmount":0.0,"forecastStart":1761130800,"windDirection":252,"windSpeed":17.481287,"humidityMax":91,"precipitationAmountByType":[],"cloudCoverLowAltPct":11,"visibilityMax":32597.847656,"cloudCover":27,"precipitationChance":0,"temperatureMin":12.223086,"cloudCoverMidAltPct":13,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"humidityMin":46,"visibilityMin":24048.089844,"precipitationType":"CLEAR","windSpeedMax":21.900928},"conditionCode":"DRIZZLE","humidityMin":46,"solarNoon":1761151214,"visibilityMin":14850.519531,"overnightForecast":{"humidity":69,"daylight":false,"forecastEnd":1761217200,"snowfallAmount":0.0,"temperatureMax":13.46,"uvIndexMin":0,"temperatureApparentMin":4.223551,"windGustSpeedMax":34.549198,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":8.525494,"precipitationAmount":0.0,"forecastStart":1761174000,"windDirection":246,"windSpeed":13.903475,"humidityMax":79,"precipitationAmountByType":[],"cloudCoverLowAltPct":1,"visibilityMax":31350.363281,"cloudCover":7,"precipitationChance":0,"temperatureMin":8.479733,"cloudCoverMidAltPct":4,"precipitationIntensityMax":0.0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"humidityMin":55,"visibilityMin":29032.0,"precipitationType":"CLEAR","windSpeedMax":16.559999},"precipitationType":"RAIN","sunriseNautical":1761128132,"sunsetCivil":1761172383,"windSpeedMax":21.900928},{"moonrise":1761226141,"moonset":1761259754,"sunrise":1761218188,"sunset":1761257023,"forecastEnd":1761278400,"sunriseCivil":1761216514,"snowfallAmount":0.0,"temperatureMax":14.238524,"solarMidnight":1761194433,"windSpeedAvg":14.478049,"temperatureMaxTime":1761244829,"windGustSpeedMax":35.729374,"precipitationAmount":0.0,"sunriseAstronomical":1761212694,"forecastStart":1761192000,"moonPhase":"WAXING_CRESCENT","humidityMax":79,"precipitationAmountByType":[],"sunsetAstronomical":1761262518,"visibilityMax":32900.125,"precipitationChance":0,"temperatureMin":8.479733,"maxUvIndex":3,"temperatureMinTime":1761217103,"sunsetNautical":1761260612,"daytimeForecast":{"humidity":62,"daylight":true,"forecastEnd":1761260400,"snowfallAmount":0.0,"temperatureMax":14.238524,"uvIndexMin":0,"temperatureApparentMin":4.345078,"windGustSpeedMax":35.729374,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":10.832659,"precipitationAmount":0.0,"forecastStart":1761217200,"windDirection":252,"windSpeed":16.204586,"humidityMax":79,"precipitationAmountByType":[],"cloudCoverLowAltPct":28,"visibilityMax":31982.833984,"cloudCover":42,"precipitationChance":0,"temperatureMin":8.48,"cloudCoverMidAltPct":31,"precipitationIntensityMax":0.0,"conditionCode":"PARTLY_CLOUDY","cloudCoverHighAltPct":0,"humidityMin":54,"visibilityMin":28920.330078,"precipitationType":"CLEAR","windSpeedMax":18.524895},"conditionCode":"PARTLY_CLOUDY","humidityMin":54,"solarNoon":1761237606,"visibilityMin":28920.330078,"overnightForecast":{"humidity":77,"daylight":false,"forecastEnd":1761303600,"snowfallAmount":0.0,"temperatureMax":12.3,"uvIndexMin":0,"temperatureApparentMin":3.822799,"windGustSpeedMax":29.8836,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":8.884628,"precipitationAmount":0.0,"forecastStart":1761260400,"windDirection":268,"windSpeed":10.912325,"humidityMax":85,"precipitationAmountByType":[],"cloudCoverLowAltPct":4,"visibilityMax":32900.125,"cloudCover":7,"precipitationChance":0,"temperatureMin":6.71,"cloudCoverMidAltPct":4,"precipitationIntensityMax":0.0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"humidityMin":62,"visibilityMin":27987.894531,"precipitationType":"CLEAR","windSpeedMax":12.4452},"precipitationType":"RAIN","sunriseNautical":1761214595,"sunsetCivil":1761258702,"windSpeedMax":18.524895},{"moonrise":1761316292,"moonset":1761348320,"sunrise":1761304656,"sunset":1761343340,"forecastEnd":1761364800,"sunriseCivil":1761302978,"snowfallAmount":0.0,"temperatureMax":14.517237,"solarMidnight":1761280824,"windSpeedAvg":10.642105,"temperatureMaxTime":1761331309,"windGustSpeedMax":30.524885,"precipitationAmount":0.0,"sunriseAstronomical":1761299156,"forecastStart":1761278400,"moonPhase":"WAXING_CRESCENT","humidityMax":85,"precipitationAmountByType":[],"sunsetAstronomical":1761348842,"visibilityMax":34697.878906,"precipitationChance":0,"temperatureMin":6.663746,"maxUvIndex":3,"temperatureMinTime":1761305412,"sunsetNautical":1761346934,"daytimeForecast":{"humidity":61,"daylight":true,"forecastEnd":1761346800,"snowfallAmount":0.0,"temperatureMax":14.517237,"uvIndexMin":0,"temperatureApparentMin":3.941025,"windGustSpeedMax":30.524885,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":12.482799,"precipitationAmount":0.0,"forecastStart":1761303600,"windDirection":284,"windSpeed":11.7716,"humidityMax":85,"precipitationAmountByType":[],"cloudCoverLowAltPct":21,"visibilityMax":34530.664062,"cloudCover":33,"precipitationChance":0,"temperatureMin":6.663746,"cloudCoverMidAltPct":29,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"humidityMin":48,"visibilityMin":27989.0,"precipitationType":"CLEAR","windSpeedMax":14.552842},"conditionCode":"MOSTLY_CLEAR","humidityMin":48,"solarNoon":1761323998,"visibilityMin":27987.894531,"overnightForecast":{"humidity":74,"daylight":false,"forecastEnd":1761390000,"snowfallAmount":0.0,"temperatureMax":12.1,"uvIndexMin":0,"temperatureApparentMin":4.741363,"windGustSpeedMax":24.6996,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":9.294739,"precipitationAmount":0.0,"forecastStart":1761346800,"windDirection":293,"windSpeed":7.757087,"humidityMax":86,"precipitationAmountByType":[],"cloudCoverLowAltPct":14,"visibilityMax":34697.878906,"cloudCover":27,"precipitationChance":0,"temperatureMin":5.98207,"cloudCoverMidAltPct":16,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":2,"humidityMin":56,"visibilityMin":29169.382812,"precipitationType":"CLEAR","windSpeedMax":10.368},"precipitationType":"RAIN","sunriseNautical":1761301058,"sunsetCivil":1761345022,"windSpeedMax":14.552842},{"moonrise":1761406274,"moonset":1761437342,"sunrise":1761391124,"sunset":1761429658,"forecastEnd":1761451200,"sunriseCivil":1761389443,"snowfallAmount":0.0,"temperatureMax":14.097731,"solarMidnight":1761367217,"windSpeedAvg":6.75635,"temperatureMaxTime":1761419435,"windGustSpeedMax":20.451601,"precipitationAmount":0.0,"sunriseAstronomical":1761385617,"forecastStart":1761364800,"moonPhase":"WAXING_CRESCENT","humidityMax":86,"precipitationAmountByType":[],"sunsetAstronomical":1761435167,"visibilityMax":34916.546875,"precipitationChance":0,"temperatureMin":5.98207,"maxUvIndex":3,"temperatureMinTime":1761389143,"sunsetNautical":1761433258,"daytimeForecast":{"humidity":61,"daylight":true,"forecastEnd":1761433200,"snowfallAmount":0.0,"temperatureMax":14.097731,"uvIndexMin":0,"temperatureApparentMin":4.790674,"windGustSpeedMax":16.968874,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":13.112373,"precipitationAmount":0.0,"forecastStart":1761390000,"windDirection":291,"windSpeed":7.593,"humidityMax":86,"precipitationAmountByType":[],"cloudCoverLowAltPct":20,"visibilityMax":34916.546875,"cloudCover":72,"precipitationChance":0,"temperatureMin":6.0,"cloudCoverMidAltPct":43,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":52,"humidityMin":48,"visibilityMin":28352.904297,"precipitationType":"CLEAR","windSpeedMax":8.575529},"conditionCode":"MOSTLY_CLOUDY","humidityMin":48,"solarNoon":1761410391,"visibilityMin":28352.904297,"overnightForecast":{"humidity":73,"daylight":false,"forecastEnd":1761476400,"snowfallAmount":0.0,"temperatureMax":12.12,"uvIndexMin":0,"temperatureApparentMin":6.938146,"windGustSpeedMax":16.372353,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":11.713829,"precipitationAmount":0.0,"forecastStart":1761433200,"windDirection":296,"windSpeed":3.83055,"humidityMax":81,"precipitationAmountByType":[],"cloudCoverLowAltPct":14,"visibilityMax":32246.925781,"cloudCover":75,"precipitationChance":0,"temperatureMin":6.873954,"cloudCoverMidAltPct":59,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":96,"humidityMin":58,"visibilityMin":25157.0,"precipitationType":"CLEAR","windSpeedMax":5.1264},"precipitationType":"RAIN","sunriseNautical":1761387521,"sunsetCivil":1761431344,"windSpeedMax":8.575529},{"moonrise":1761495938,"moonset":1761526859,"sunrise":1761477593,"sunset":1761515978,"forecastEnd":1761537600,"sunriseCivil":1761475908,"snowfallAmount":0.0,"temperatureMax":13.757895,"solarMidnight":1761453610,"windSpeedAvg":5.215644,"temperatureMaxTime":1761504319,"windGustSpeedMax":13.48378,"precipitationAmount":0.0,"sunriseAstronomical":1761472079,"forecastStart":1761451200,"moonPhase":"WAXING_CRESCENT","humidityMax":85,"precipitationAmountByType":[],"sunsetAstronomical":1761521493,"visibilityMax":35069.429688,"precipitationChance":0,"temperatureMin":6.873954,"maxUvIndex":3,"temperatureMinTime":1761473869,"sunsetNautical":1761519583,"daytimeForecast":{"humidity":65,"daylight":true,"forecastEnd":1761519600,"snowfallAmount":0.0,"temperatureMax":13.757895,"uvIndexMin":0,"temperatureApparentMin":6.960814,"windGustSpeedMax":13.48378,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":13.043096,"precipitationAmount":0.0,"forecastStart":1761476400,"windDirection":188,"windSpeed":5.7639,"humidityMax":81,"precipitationAmountByType":[],"cloudCoverLowAltPct":39,"visibilityMax":31860.765625,"cloudCover":81,"precipitationChance":0,"temperatureMin":6.94,"cloudCoverMidAltPct":79,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":60,"humidityMin":56,"visibilityMin":24641.685547,"precipitationType":"CLEAR","windSpeedMax":6.216825},"conditionCode":"MOSTLY_CLOUDY","humidityMin":56,"solarNoon":1761496785,"visibilityMin":24641.685547,"overnightForecast":{"humidity":85,"daylight":false,"forecastEnd":1761562800,"snowfallAmount":0.0,"temperatureMax":11.81,"uvIndexMin":0,"temperatureApparentMin":7.008282,"windGustSpeedMax":10.8612,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":11.379537,"precipitationAmount":0.0,"forecastStart":1761519600,"windDirection":357,"windSpeed":7.151888,"humidityMax":92,"precipitationAmountByType":[],"cloudCoverLowAltPct":56,"visibilityMax":35069.429688,"cloudCover":77,"precipitationChance":0,"temperatureMin":8.639686,"cloudCoverMidAltPct":84,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":18,"humidityMin":67,"visibilityMin":28417.804688,"precipitationType":"CLEAR","windSpeedMax":10.1268},"precipitationType":"RAIN","sunriseNautical":1761473984,"sunsetCivil":1761517667,"windSpeedMax":6.216825},{"moonrise":1761585176,"moonset":1761616820,"sunrise":1761564062,"sunset":1761602300,"forecastEnd":1761624000,"sunriseCivil":1761562373,"snowfallAmount":0.0,"temperatureMax":16.050341,"solarMidnight":1761540004,"windSpeedAvg":9.453151,"temperatureMaxTime":1761591724,"windGustSpeedMax":20.173061,"precipitationAmount":0.0,"sunriseAstronomical":1761558540,"forecastStart":1761537600,"moonPhase":"WAXING_CRESCENT","humidityMax":92,"precipitationAmountByType":[],"sunsetAstronomical":1761607821,"visibilityMax":36322.265625,"precipitationChance":0,"temperatureMin":8.639686,"maxUvIndex":3,"temperatureMinTime":1761555828,"sunsetNautical":1761605911,"daytimeForecast":{"humidity":70,"daylight":true,"forecastEnd":1761606000,"snowfallAmount":0.0,"temperatureMax":16.050341,"uvIndexMin":0,"temperatureApparentMin":7.033569,"windGustSpeedMax":19.7316,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":3,"temperatureApparentMax":15.328562,"precipitationAmount":0.0,"forecastStart":1761562800,"windDirection":54,"windSpeed":9.641274,"humidityMax":90,"precipitationAmountByType":[],"cloudCoverLowAltPct":48,"visibilityMax":36322.265625,"cloudCover":69,"precipitationChance":0,"temperatureMin":8.88,"cloudCoverMidAltPct":39,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":9,"humidityMin":60,"visibilityMin":28688.398438,"precipitationType":"CLEAR","windSpeedMax":10.742399},"conditionCode":"MOSTLY_CLOUDY","humidityMin":60,"solarNoon":1761583180,"visibilityMin":28688.398438,"overnightForecast":{"humidity":84,"daylight":false,"forecastEnd":1761649200,"snowfallAmount":0.0,"temperatureMax":13.64,"uvIndexMin":0,"temperatureApparentMin":3.542812,"windGustSpeedMax":20.173061,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":0,"temperatureApparentMax":11.504132,"precipitationAmount":0.0,"forecastStart":1761606000,"windDirection":33,"windSpeed":10.611563,"humidityMax":94,"precipitationAmountByType":[],"cloudCoverLowAltPct":14,"visibilityMax":34023.632812,"cloudCover":41,"precipitationChance":0,"temperatureMin":6.501806,"cloudCoverMidAltPct":3,"precipitationIntensityMax":0.0,"conditionCode":"PARTLY_CLOUDY","cloudCoverHighAltPct":0,"humidityMin":70,"visibilityMin":25308.0,"precipitationType":"CLEAR","windSpeedMax":11.312479},"precipitationType":"RAIN","sunriseNautical":1761560447,"sunsetCivil":1761603991,"windSpeedMax":11.312479},{"moonrise":1761673972,"moonset":1761707103,"sunrise":1761650531,"sunset":1761688623,"forecastEnd":1761710400,"sunriseCivil":1761648838,"snowfallAmount":0.0,"temperatureMax":16.220354,"solarMidnight":1761626399,"windSpeedAvg":10.950863,"temperatureMaxTime":1761677832,"windGustSpeedMax":31.8384,"precipitationAmount":0.0,"sunriseAstronomical":1761645002,"forecastStart":1761624000,"moonPhase":"WAXING_CRESCENT","humidityMax":94,"precipitationAmountByType":[],"sunsetAstronomical":1761694150,"visibilityMax":35435.453125,"precipitationChance":0,"temperatureMin":6.501806,"maxUvIndex":4,"temperatureMinTime":1761648693,"sunsetNautical":1761692239,"daytimeForecast":{"humidity":69,"daylight":true,"forecastEnd":1761692400,"snowfallAmount":0.0,"temperatureMax":16.220354,"uvIndexMin":0,"temperatureApparentMin":3.612268,"windGustSpeedMax":28.4904,"perceivedPrecipitationIntensityMax":0.0,"uvIndexMax":4,"temperatureApparentMax":16.101068,"precipitationAmount":0.0,"forecastStart":1761649200,"windDirection":65,"windSpeed":10.691,"humidityMax":94,"precipitationAmountByType":[],"cloudCoverLowAltPct":17,"visibilityMax":35435.453125,"cloudCover":33,"precipitationChance":0,"temperatureMin":6.51,"cloudCoverMidAltPct":16,"precipitationIntensityMax":0.0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":6,"humidityMin":56,"visibilityMin":16905.0,"precipitationType":"CLEAR","windSpeedMax":11.379614},"conditionCode":"MOSTLY_CLEAR","humidityMin":56,"solarNoon":1761669575,"visibilityMin":14173.703125,"overnightForecast":{"humidity":85,"daylight":false,"forecastEnd":1761735600,"snowfallAmount":0.0,"temperatureMax":13.19,"uvIndexMin":0,"temperatureApparentMin":4.16664,"windGustSpeedMax":38.610031,"perceivedPrecipitationIntensityMax":0.804,"uvIndexMax":0,"temperatureApparentMax":10.807237,"precipitationAmount":2.414,"forecastStart":1761692400,"windDirection":90,"windSpeed":12.916863,"humidityMax":94,"precipitationAmountByType":[{"expected":2.414,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"cloudCoverLowAltPct":88,"visibilityMax":20582.857422,"cloudCover":64,"precipitationChance":24,"temperatureMin":7.55,"cloudCoverMidAltPct":93,"precipitationIntensityMax":0.541758,"conditionCode":"MOSTLY_CLOUDY","cloudCoverHighAltPct":43,"humidityMin":69,"visibilityMin":13093.134766,"precipitationType":"RAIN","windSpeedMax":14.274548},"precipitationType":"RAIN","sunriseNautical":1761646910,"sunsetCivil":1761690316,"windSpeedMax":13.402707},{"moonrise":1761762382,"sunrise":1761737000,"sunset":1761774947,"forecastEnd":1761796800,"sunriseCivil":1761735303,"snowfallAmount":0.0,"temperatureMax":16.245462,"solarMidnight":1761712795,"windSpeedAvg":16.424599,"temperatureMaxTime":1761767437,"windGustSpeedMax":50.617004,"precipitationAmount":7.737,"sunriseAstronomical":1761731463,"forecastStart":1761710400,"moonPhase":"FIRST_QUARTER","humidityMax":94,"precipitationAmountByType":[{"expected":7.737,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"sunsetAstronomical":1761780481,"visibilityMax":24203.0,"precipitationChance":51,"temperatureMin":4.73919,"maxUvIndex":3,"temperatureMinTime":1761742843,"sunsetNautical":1761778569,"daytimeForecast":{"humidity":74,"daylight":true,"forecastEnd":1761778800,"snowfallAmount":0.0,"temperatureMax":16.245462,"uvIndexMin":0,"temperatureApparentMin":2.770287,"windGustSpeedMax":50.616001,"perceivedPrecipitationIntensityMax":0.8,"uvIndexMax":3,"temperatureApparentMax":14.084887,"precipitationAmount":5.039,"forecastStart":1761735600,"windDirection":87,"windSpeed":15.368976,"humidityMax":94,"precipitationAmountByType":[{"expected":5.039,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"cloudCoverLowAltPct":100,"visibilityMax":22176.816406,"cloudCover":48,"precipitationChance":39,"temperatureMin":4.73919,"cloudCoverMidAltPct":48,"precipitationIntensityMax":0.536092,"conditionCode":"DRIZZLE","cloudCoverHighAltPct":0,"humidityMin":63,"visibilityMin":11223.716797,"precipitationType":"RAIN","windSpeedMax":17.1576},"conditionCode":"DRIZZLE","humidityMin":63,"solarNoon":1761755971,"visibilityMin":11223.716797,"overnightForecast":{"humidity":84,"daylight":false,"forecastEnd":1761822000,"snowfallAmount":0.0,"temperatureMax":14.52,"uvIndexMin":0,"temperatureApparentMin":0.034572,"windGustSpeedMax":50.617004,"perceivedPrecipitationIntensityMax":0.601,"uvIndexMax":0,"temperatureApparentMax":10.488429,"precipitationAmount":0.284,"forecastStart":1761778800,"windDirection":356,"windSpeed":17.016912,"humidityMax":88,"precipitationAmountByType":[{"expected":0.284,"minimumSnow":0.0,"maximumSnow":0.0,"precipitationType":"RAIN","expectedSnow":0.0}],"cloudCoverLowAltPct":47,"visibilityMax":24203.740234,"cloudCover":40,"precipitationChance":24,"temperatureMin":5.428105,"cloudCoverMidAltPct":0,"precipitationIntensityMax":0.284,"conditionCode":"PARTLY_CLOUDY","cloudCoverHighAltPct":0,"humidityMin":74,"visibilityMin":22040.0,"precipitationType":"RAIN","windSpeedMax":27.57523},"precipitationType":"RAIN","sunriseNautical":1761733373,"sunsetCivil":1761776643,"windSpeedMax":27.57523}]} """ + static let historicalComparisonJSON = """ + {"metadata":{"latitude":40.709999,"longitude":-74.010002,"attributionUrl":"https://developer.apple.com/weatherkit/data-source-attribution/","readTime":1761922785,"expireTime":1761926308,"temporarilyUnavailable":false,"sourceType":"MODELED","reportedTime":1761915934},"comparisons":[{"condition":"TEMPERATURE_MAX","deviation":"NORMAL","baselineValue":14.195007,"baselineStartDate":0,"currentValue":15.09,"baselineType":"MEAN"},{"condition":"PRECIPITATION","deviation":"NORMAL","baselineValue":0.0,"baselineStartDate":0,"currentValue":0.0,"baselineType":"MEAN"},{"condition":"UVI","deviation":"NORMAL","baselineValue":3.0,"baselineStartDate":0,"currentValue":3.0,"baselineType":"MEAN"}]} + """ + + static let changesJSON = """ + {"metadata":{"latitude":40.709999,"longitude":-74.010002,"attributionUrl":"https://developer.apple.com/weatherkit/data-source-attribution/","readTime":1761916879,"expireTime":1761920355,"temporarilyUnavailable":false,"sourceType":"MODELED","reportedTime":1761908735},"changes":[{"forecastEnd":1762056000,"maxTemperatureChange":"STEADY","forecastStart":1761969600,"minTemperatureChange":"STEADY","dayPrecipitationChange":"STEADY","nightPrecipitationChange":"STEADY"},{"forecastEnd":1762146000,"maxTemperatureChange":"STEADY","forecastStart":1762056000,"minTemperatureChange":"STEADY","dayPrecipitationChange":"STEADY","nightPrecipitationChange":"STEADY"}],"forecastEnd":1762146000,"forecastStart":1761969600} + """ + static let hourlyWeatherJSON = """ {"metadata":{"latitude":40.709999,"longitude":-74.010002,"attributionUrl":"https://developer.apple.com/weatherkit/data-source-attribution/","readTime":1760993102,"expireTime":1760996421,"temporarilyUnavailable":false,"sourceType":"MODELED","reportedTime":1760961934},"hours":[{"daylight":true,"humidity":60,"pressure":1009.330017,"temperature":16.59,"visibility":34214.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":66,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":8.792908,"windGust":49.967999,"temperatureApparent":11.496218,"cloudCover":74,"precipitationChance":0,"cloudCoverMidAltPct":50,"conditionCode":"WINDY","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1760990400,"pressureTrend":"RISING","windDirection":276,"uvIndex":1,"windSpeed":25.819199,"snowfallIntensity":0.0},{"daylight":true,"humidity":59,"pressure":1010.030029,"temperature":16.4,"visibility":34652.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":29,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":8.365891,"windGust":48.132,"temperatureApparent":11.785448,"cloudCover":61,"precipitationChance":0,"cloudCoverMidAltPct":43,"conditionCode":"PARTLY_CLOUDY","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1760994000,"pressureTrend":"RISING","windDirection":276,"uvIndex":0,"windSpeed":24.0336,"snowfallIntensity":0.0},{"daylight":true,"humidity":58,"pressure":1010.640015,"temperature":15.91,"visibility":34401.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":24,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":7.654709,"windGust":42.588001,"temperatureApparent":10.978209,"cloudCover":34,"precipitationChance":0,"cloudCoverMidAltPct":14,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1760997600,"pressureTrend":"RISING","windDirection":276,"uvIndex":0,"windSpeed":20.6964,"snowfallIntensity":0.0},{"daylight":false,"humidity":60,"pressure":1011.280029,"temperature":15.22,"visibility":34123.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":26,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":7.503769,"windGust":38.051998,"temperatureApparent":10.461295,"cloudCover":27,"precipitationChance":0,"cloudCoverMidAltPct":1,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761001200,"pressureTrend":"RISING","windDirection":269,"uvIndex":0,"windSpeed":18.7848,"snowfallIntensity":0.0},{"daylight":false,"humidity":62,"pressure":1012.01001,"temperature":14.52,"visibility":33566.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":13,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":7.323807,"windGust":36.792,"temperatureApparent":9.444345,"cloudCover":16,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"MOSTLY_CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761004800,"pressureTrend":"RISING","windDirection":264,"uvIndex":0,"windSpeed":18.9828,"snowfallIntensity":0.0},{"daylight":false,"humidity":63,"pressure":1012.51001,"temperature":13.86,"visibility":33488.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":4,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.933304,"windGust":31.406401,"temperatureApparent":8.909959,"cloudCover":8,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761008400,"pressureTrend":"RISING","windDirection":264,"uvIndex":0,"windSpeed":17.4312,"snowfallIntensity":0.0},{"daylight":false,"humidity":64,"pressure":1012.969971,"temperature":13.26,"visibility":33081.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":1,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.594101,"windGust":29.599201,"temperatureApparent":8.454525,"cloudCover":3,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761012000,"pressureTrend":"RISING","windDirection":264,"uvIndex":0,"windSpeed":16.232399,"snowfallIntensity":0.0},{"daylight":false,"humidity":66,"pressure":1013.210022,"temperature":12.71,"visibility":32809.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.518906,"windGust":30.6684,"temperatureApparent":8.139853,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761015600,"pressureTrend":"RISING","windDirection":266,"uvIndex":0,"windSpeed":14.9436,"snowfallIntensity":0.0},{"daylight":false,"humidity":67,"pressure":1013.330017,"temperature":12.16,"visibility":32719.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.213058,"windGust":29.07,"temperatureApparent":7.687512,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761019200,"pressureTrend":"RISING","windDirection":264,"uvIndex":0,"windSpeed":14.3208,"snowfallIntensity":0.0},{"daylight":false,"humidity":68,"pressure":1013.440002,"temperature":11.64,"visibility":32487.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.931213,"windGust":26.874001,"temperatureApparent":7.438548,"cloudCover":1,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761022800,"pressureTrend":"STEADY","windDirection":263,"uvIndex":0,"windSpeed":13.3884,"snowfallIntensity":0.0},{"daylight":false,"humidity":69,"pressure":1013.570007,"temperature":11.22,"visibility":32417.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.740585,"windGust":25.430401,"temperatureApparent":7.126996,"cloudCover":1,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761026400,"pressureTrend":"STEADY","windDirection":260,"uvIndex":0,"windSpeed":12.996,"snowfallIntensity":0.0},{"daylight":false,"humidity":70,"pressure":1013.619995,"temperature":10.8,"visibility":32285.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.545868,"windGust":26.319599,"temperatureApparent":6.737651,"cloudCover":1,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761030000,"pressureTrend":"STEADY","windDirection":255,"uvIndex":0,"windSpeed":12.8484,"snowfallIntensity":0.0},{"daylight":false,"humidity":72,"pressure":1013.799988,"temperature":10.37,"visibility":31974.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.539154,"windGust":27.4104,"temperatureApparent":6.219483,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761033600,"pressureTrend":"STEADY","windDirection":254,"uvIndex":0,"windSpeed":13.003201,"snowfallIntensity":0.0},{"daylight":false,"humidity":74,"pressure":1014.099976,"temperature":9.99,"visibility":31442.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.568222,"windGust":27.396,"temperatureApparent":5.870001,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761037200,"pressureTrend":"STEADY","windDirection":254,"uvIndex":0,"windSpeed":12.87,"snowfallIntensity":0.0},{"daylight":false,"humidity":75,"pressure":1014.380005,"temperature":9.71,"visibility":31246.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.491318,"windGust":26.218801,"temperatureApparent":5.855513,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761040800,"pressureTrend":"RISING","windDirection":254,"uvIndex":0,"windSpeed":12.121201,"snowfallIntensity":0.0},{"daylight":false,"humidity":76,"pressure":1014.659973,"temperature":9.48,"visibility":31188.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.459732,"windGust":24.0804,"temperatureApparent":5.913912,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761044400,"pressureTrend":"RISING","windDirection":251,"uvIndex":0,"windSpeed":11.350801,"snowfallIntensity":0.0},{"daylight":true,"humidity":76,"pressure":1014.859985,"temperature":9.7,"visibility":31016.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":5.67276,"windGust":22.258801,"temperatureApparent":8.335776,"cloudCover":3,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":8,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761048000,"pressureTrend":"RISING","windDirection":248,"uvIndex":0,"windSpeed":11.0484,"snowfallIntensity":0.0},{"daylight":true,"humidity":73,"pressure":1014.859985,"temperature":10.8,"visibility":31345.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.152725,"windGust":21.1464,"temperatureApparent":11.030827,"cloudCover":7,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":18,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761051600,"pressureTrend":"STEADY","windDirection":245,"uvIndex":1,"windSpeed":10.314,"snowfallIntensity":0.0},{"daylight":true,"humidity":67,"pressure":1014.549988,"temperature":12.3,"visibility":31755.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.346619,"windGust":21.499201,"temperatureApparent":13.069326,"cloudCover":1,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761055200,"pressureTrend":"STEADY","windDirection":237,"uvIndex":2,"windSpeed":10.173599,"snowfallIntensity":0.0},{"daylight":true,"humidity":60,"pressure":1013.969971,"temperature":14.08,"visibility":32450.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.43071,"windGust":23.270401,"temperatureApparent":14.748355,"cloudCover":0,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":0,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761058800,"pressureTrend":"FALLING","windDirection":227,"uvIndex":3,"windSpeed":10.98,"snowfallIntensity":0.0},{"daylight":true,"humidity":55,"pressure":1013.330017,"temperature":15.88,"visibility":32876.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.849945,"windGust":25.5348,"temperatureApparent":16.206793,"cloudCover":1,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761062400,"pressureTrend":"FALLING","windDirection":215,"uvIndex":4,"windSpeed":12.1608,"snowfallIntensity":0.0},{"daylight":true,"humidity":51,"pressure":1012.409973,"temperature":17.190001,"visibility":32948.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":6.963364,"windGust":29.120398,"temperatureApparent":17.062799,"cloudCover":2,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761066000,"pressureTrend":"FALLING","windDirection":203,"uvIndex":4,"windSpeed":13.9572,"snowfallIntensity":0.0},{"daylight":true,"humidity":48,"pressure":1011.390015,"temperature":18.35,"visibility":33098.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":7.144775,"windGust":32.410801,"temperatureApparent":17.932598,"cloudCover":2,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761069600,"pressureTrend":"FALLING","windDirection":194,"uvIndex":4,"windSpeed":15.5196,"snowfallIntensity":0.0},{"daylight":true,"humidity":48,"pressure":1010.530029,"temperature":18.9,"visibility":33185.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":7.648605,"windGust":33.883198,"temperatureApparent":18.295763,"cloudCover":2,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761073200,"pressureTrend":"FALLING","windDirection":185,"uvIndex":2,"windSpeed":16.3692,"snowfallIntensity":0.0},{"daylight":true,"humidity":50,"pressure":1010.0,"temperature":19.110001,"visibility":32799.0,"snowfallAmount":0.0,"cloudCoverLowAltPct":0,"perceivedPrecipitationIntensity":0.0,"precipitationIntensity":0.0,"temperatureDewPoint":8.442184,"windGust":33.670799,"temperatureApparent":18.231922,"cloudCover":2,"precipitationChance":0,"cloudCoverMidAltPct":0,"conditionCode":"CLEAR","cloudCoverHighAltPct":2,"precipitationType":"CLEAR","precipitationAmount":0.0,"forecastStart":1761076800,"pressureTrend":"FALLING","windDirection":177,"uvIndex":1,"windSpeed":16.948801,"snowfallIntensity":0.0}]} """ From ffd59991899c406b8271aa0339d05a7fc841889d Mon Sep 17 00:00:00 2001 From: Jeremy Greenwood Date: Wed, 5 Nov 2025 14:36:35 -0500 Subject: [PATCH 06/11] bugfixes and test cases --- Package.swift | 5 +- .../Internal/Models/APIMetadata.swift | 2 +- .../Internal/NetworkClient.swift | 12 +- .../Public/WeatherService.swift | 4 +- .../OpenWeatherKitTests.swift | 147 ++++++++++++++++++ .../Utils/MockClient.swift | 42 ++++- .../OpenWeatherKitTests/Utils/MockData.swift | 33 +++- 7 files changed, 230 insertions(+), 15 deletions(-) diff --git a/Package.swift b/Package.swift index f9d5c82..afd661e 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,10 @@ let package = Package( .target( name: "OpenWeatherKit", dependencies: [], - resources: [.process("Resources")] + resources: [.process("Resources")], + swiftSettings: [ + .unsafeFlags(["-Xfrontend", "-enable-pack-metadata-stack-promotion=false"]) // https://github.com/swiftlang/swift/issues/67702 + ] ), .testTarget( name: "OpenWeatherKitTests", diff --git a/Sources/OpenWeatherKit/Internal/Models/APIMetadata.swift b/Sources/OpenWeatherKit/Internal/Models/APIMetadata.swift index 1df1288..4be282e 100644 --- a/Sources/OpenWeatherKit/Internal/Models/APIMetadata.swift +++ b/Sources/OpenWeatherKit/Internal/Models/APIMetadata.swift @@ -10,7 +10,7 @@ import Foundation // MARK: - APIMetadata struct APIMetadata: Codable, Equatable { @TextCaseCoding var sourceType: String - let attributionURL: String + let attributionURL: String? let expireTime: Date let language: String? let latitude: Double diff --git a/Sources/OpenWeatherKit/Internal/NetworkClient.swift b/Sources/OpenWeatherKit/Internal/NetworkClient.swift index 4dbf7fe..41970c5 100644 --- a/Sources/OpenWeatherKit/Internal/NetworkClient.swift +++ b/Sources/OpenWeatherKit/Internal/NetworkClient.swift @@ -73,8 +73,8 @@ struct NetworkClient: Sendable { let queryItems = [ URLQueryItem(name: "dataSets", value: names.joined(separator: ",")), - URLQueryItem(name: "startHour", value: "\(startHour)"), - URLQueryItem(name: "endHour", value: "\(endHour)") + URLQueryItem(name: "start", value: "\(startHour)"), + URLQueryItem(name: "end", value: "\(endHour)") ] return try await get( @@ -98,8 +98,8 @@ struct NetworkClient: Sendable { let queryItems = [ URLQueryItem(name: "dataSets", value: names.joined(separator: ",")), - URLQueryItem(name: "startDay", value: "\(startDay)"), - URLQueryItem(name: "endDay", value: "\(endDay)") + URLQueryItem(name: "start", value: "\(startDay)"), + URLQueryItem(name: "end", value: "\(endDay)") ] return try await get( @@ -123,8 +123,8 @@ struct NetworkClient: Sendable { let queryItems = [ URLQueryItem(name: "dataSets", value: names.joined(separator: ",")), - URLQueryItem(name: "startMonth", value: "\(startMonth)"), - URLQueryItem(name: "endMonth", value: "\(endMonth)") + URLQueryItem(name: "start", value: "\(startMonth)"), + URLQueryItem(name: "end", value: "\(endMonth)") ] return try await get( diff --git a/Sources/OpenWeatherKit/Public/WeatherService.swift b/Sources/OpenWeatherKit/Public/WeatherService.swift index 9874255..1038da3 100644 --- a/Sources/OpenWeatherKit/Public/WeatherService.swift +++ b/Sources/OpenWeatherKit/Public/WeatherService.swift @@ -27,7 +27,7 @@ final public class WeatherService: Sendable { case germanDE = "de_DE" } - public var jwt: @Sendable () -> String + public var jwt: @Sendable () async throws -> String public var language: Language /// Initializes an instance of Configuation @@ -35,7 +35,7 @@ final public class WeatherService: Sendable { /// - jwt: A closure to provide a JWT. /// - language: A language to localize human readable strings. public init( - jwt: @escaping @Sendable () -> String, + jwt: @escaping @Sendable () async throws -> String, language: WeatherService.Configuration.Language = .englishUS ) { self.jwt = jwt diff --git a/Tests/OpenWeatherKitTests/OpenWeatherKitTests.swift b/Tests/OpenWeatherKitTests/OpenWeatherKitTests.swift index 325b6c2..75da4a5 100644 --- a/Tests/OpenWeatherKitTests/OpenWeatherKitTests.swift +++ b/Tests/OpenWeatherKitTests/OpenWeatherKitTests.swift @@ -112,6 +112,81 @@ final class OpenWeatherKitTests: XCTestCase { } } } + + func testHourlyStatistics() async throws { + let networkClient = NetworkClient( + client: MockClient(include: Set([])) + ) + + let service = WeatherService( + configuration: .init(jwt: { "" }), + networkClient: networkClient, + geocoder: .mock + ) + + let _ = try await service.hourlyStatistics( + for: Location( + latitude: 0, + longitude: 0), + including: .temperature) + } + + func testDailyStatistics() async throws { + let networkClient = NetworkClient( + client: MockClient(include: Set([])) + ) + + let service = WeatherService( + configuration: .init(jwt: { "" }), + networkClient: networkClient, + geocoder: .mock + ) + + let _ = try await service.dailyStatistics( + for: Location( + latitude: 0, + longitude: 0), + including: .temperature, .precipitation + ) + } + + func testMonthlyStatistics() async throws { + let networkClient = NetworkClient( + client: MockClient(include: Set([])) + ) + + let service = WeatherService( + configuration: .init(jwt: { "" }), + networkClient: networkClient, + geocoder: .mock + ) + + let _ = try await service.monthlyStatistics( + for: Location( + latitude: 0, + longitude: 0), + including: .temperature, .precipitation + ) + } + + func testDailySummary() async throws { + let networkClient = NetworkClient( + client: MockClient(include: Set([])) + ) + + let service = WeatherService( + configuration: .init(jwt: { "" }), + networkClient: networkClient, + geocoder: .mock + ) + + let _ = try await service.dailySummary( + for: Location( + latitude: 0, + longitude: 0), + including: .temperature, .precipitation + ) + } #else func testWeather() async throws { let networkClient = NetworkClient( @@ -217,5 +292,77 @@ final class OpenWeatherKitTests: XCTestCase { } } } + + func testDailySummary() async throws { + let networkClient = NetworkClient( + client: MockClient(include: Set([])) + ) + + let service = WeatherService( + configuration: .init(jwt: { "" }), + networkClient: networkClient + ) + + let _ = try await service.dailySummary( + for: Location( + latitude: 0, + longitude: 0), + including: .temperature, .precipitation + ) + } + + func testHourlyStatistics() async throws { + let networkClient = NetworkClient( + client: MockClient(include: Set([])) + ) + + let service = WeatherService( + configuration: .init(jwt: { "" }), + networkClient: networkClient + ) + + let _ = try await service.hourlyStatistics( + for: Location( + latitude: 0, + longitude: 0), + including: .temperature) + } + + func testDailyStatistics() async throws { + let networkClient = NetworkClient( + client: MockClient(include: Set([])) + ) + + let service = WeatherService( + configuration: .init(jwt: { "" }), + networkClient: networkClient + ) + + let _ = try await service.dailyStatistics( + for: Location( + latitude: 0, + longitude: 0), + including: .temperature, .precipitation + ) + } + + func testMonthlyStatistics() async throws { + let networkClient = NetworkClient( + client: MockClient(include: Set([])) + ) + + let service = WeatherService( + configuration: .init(jwt: { "" }), + networkClient: networkClient + ) + + let _ = try await service.monthlyStatistics( + for: Location( + latitude: 0, + longitude: 0), + including: .temperature, .precipitation + ) + } #endif } + diff --git a/Tests/OpenWeatherKitTests/Utils/MockClient.swift b/Tests/OpenWeatherKitTests/Utils/MockClient.swift index 95edaec..4b5fec3 100644 --- a/Tests/OpenWeatherKitTests/Utils/MockClient.swift +++ b/Tests/OpenWeatherKitTests/Utils/MockClient.swift @@ -1,6 +1,6 @@ // // MockClient.swift -// +// // // Created by Jeremy Greenwood on 11/9/22. // @@ -19,11 +19,13 @@ actor MockClient: Client { } enum Include: CaseIterable { + case alerts + case changes case current case daily + case historicalComparisons case hourly case nextHour - case alerts } var include: Set @@ -39,11 +41,33 @@ actor MockClient: Client { MockData.availability, allocator: .init() ) - } else { + } else if request.url.contains("/weather/") { return try! encoder.encodeAsByteBuffer( Self.apiWeather(with: include), allocator: .init() ) + } else if request.url.contains("/summary/") { + return try! encoder.encodeAsByteBuffer( + MockData.dailySummary, + allocator: .init() + ) + } else if request.url.contains("/statistics/hourly/") { + return try! encoder.encodeAsByteBuffer( + MockData.hourlyStatistics, + allocator: .init() + ) + } else if request.url.contains("/statistics/daily/") { + return try! encoder.encodeAsByteBuffer( + MockData.dailyStatistics, + allocator: .init() + ) + } else if request.url.contains("/statistics/monthly/") { + return try! encoder.encodeAsByteBuffer( + MockData.monthlyStatistics, + allocator: .init() + ) + } else { + preconditionFailure("Unknown URL: \(request.url)") } }() @@ -59,8 +83,18 @@ actor MockClient: Client { let data: Data = { if request.url!.absoluteString.contains("/availability/") { return try! encoder.encode(MockData.availability) - } else { + } else if request.url!.absoluteString.contains("/weather/") { return try! encoder.encode(Self.apiWeather(with: include)) + } else if request.url!.absoluteString.contains("/summary/") { + return try! encoder.encode(MockData.dailySummary) + } else if request.url!.absoluteString.contains("/statistics/hourly/") { + preconditionFailure() + } else if request.url!.absoluteString.contains("/statistics/daily/") { + preconditionFailure() + } else if request.url!.absoluteString.contains("/statistics/monthly/") { + preconditionFailure() + } else { + preconditionFailure("Unknown URL: \(request.url!.absoluteString)") } }() diff --git a/Tests/OpenWeatherKitTests/Utils/MockData.swift b/Tests/OpenWeatherKitTests/Utils/MockData.swift index 58d2b8a..b1198e6 100644 --- a/Tests/OpenWeatherKitTests/Utils/MockData.swift +++ b/Tests/OpenWeatherKitTests/Utils/MockData.swift @@ -1,6 +1,6 @@ // // MockData.swift -// +// // // Created by Jeremy Greenwood on 11/9/22. // @@ -21,6 +21,10 @@ struct MockData { decode(APIForecastDaily.self, from: dailyWeatherJSON) } + static var dailySummary: APIDailySummary { + decode(APIDailySummary.self, from: dailySummaryJSON) + } + static var changes: APIWeatherChanges { decode(APIWeatherChanges.self, from: changesJSON) } @@ -40,6 +44,18 @@ struct MockData { static var alerts: APIWeatherAlerts { decode(APIWeatherAlerts.self, from: alertJSON) } + + static var hourlyStatistics: APIHourlyStatistics { + decode(APIHourlyStatistics.self, from: hourlyStatisticsJSON) + } + + static var dailyStatistics: APIDailyStatistics { + decode(APIDailyStatistics.self, from: dailyStatisticsJSON) + } + + static var monthlyStatistics: APIMonthlyStatistics { + decode(APIMonthlyStatistics.self, from: monthlyStatisticsJSON) + } } fileprivate extension MockData { @@ -81,8 +97,23 @@ fileprivate extension MockData { {"metadata":{"latitude":37.541,"longitude":-77.435997,"expireTime":1761153822,"sourceType":"MODELED","attributionUrl":"https://developer.apple.com/weatherkit/data-source-attribution/","readTime":1761146622,"temporarilyUnavailable":false,"providerName":"US National Oceanic & Atmospheric Administration","reportedTime":1761146280},"condition":[{"parameters":[],"startTime":1761146580,"beginCondition":"CLEAR","forecastToken":"CLEAR","endCondition":"CLEAR"}],"summary":[{"condition":"CLEAR","precipitationChance":0,"startTime":1761146580,"precipitationIntensity":0.0}],"minutes":[{"precipitationChance":0,"startTime":1761146580,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146640,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146700,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146760,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146820,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146880,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761146940,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147000,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147060,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147120,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147180,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147240,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147300,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147360,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147420,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147480,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147540,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147600,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147660,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147720,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147780,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147840,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147900,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761147960,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148020,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148080,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148140,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148200,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148260,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148320,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148380,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148440,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148500,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148560,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148620,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148680,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148740,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148800,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148860,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148920,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761148980,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149040,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149100,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149160,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149220,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149280,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149340,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149400,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149460,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149520,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149580,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149640,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149700,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149760,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149820,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149880,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761149940,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150000,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150060,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150120,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150180,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150240,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150300,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150360,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150420,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150480,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150540,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150600,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150660,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150720,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150780,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150840,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150900,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761150960,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151020,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151080,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151140,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151200,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151260,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151320,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151380,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151440,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151500,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151560,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0},{"precipitationChance":0,"startTime":1761151620,"precipitationIntensity":0.0,"perceivedPrecipitationIntensity":0.0}],"forecastEnd":1761151680,"forecastStart":1761146580} """ + static let dailySummaryJSON = """ + {"metadata":{"latitude":40.709999,"longitude":-74.005997,"readTime":1762359867,"expireTime":1762446267,"temporarilyUnavailable":false,"sourceType":"MODELED","reportedTime":1762359867},"days":[{"date":20386,"temperatureMin":9.379158,"precipitationAmount":0.0,"snowfallAmount":0.0,"temperatureMax":15.36186},{"date":20387,"temperatureMin":6.276531,"precipitationAmount":0.0,"snowfallAmount":0.0,"temperatureMax":13.68008},{"date":20388,"temperatureMin":6.78221,"precipitationAmount":0.0,"snowfallAmount":0.0,"temperatureMax":12.255963},{"date":20389,"temperatureMin":9.069812,"precipitationAmount":0.003083,"snowfallAmount":0.0,"temperatureMax":12.601282},{"date":20390,"temperatureMin":10.797453,"precipitationAmount":54.842918,"snowfallAmount":0.0,"temperatureMax":16.799917},{"date":20391,"temperatureMin":10.667031,"precipitationAmount":0.093583,"snowfallAmount":0.0,"temperatureMax":14.97},{"date":20392,"temperatureMin":9.081073,"precipitationAmount":0.0,"snowfallAmount":0.0,"temperatureMax":14.824274},{"date":20393,"temperatureMin":6.265488,"precipitationAmount":0.0,"snowfallAmount":0.0,"temperatureMax":15.495725}],"endDate":20393,"startDate":20386} + """ + static let alertJSON = """ {"metadata":{"language":"en","latitude":37.541,"longitude":-77.435997,"expireTime":1761146922,"sourceType":"STATION","attributionUrl":"https://developer.apple.com/weatherkit/data-source-attribution/","readTime":1761146622,"temporarilyUnavailable":false,"providerName":"National Weather Service","reportedTime":1761115740},"alerts":[{"id":"9b2651f9-dd76-5874-beac-98c5a8465d4a","description":"Special Weather Statement","token":"SPECIAL_WEATHER_STATEMENT","phenomenon":"Other","severity":"MODERATE","significance":"UNKNOWN","source":"National Weather Service","urgency":"EXPECTED","certainty":"OBSERVED","importance":"NORMAL","responses":["EXECUTE"],"eventOnsetTime":1761115740,"detailsUrl":"https://weatherkit.apple.com/alertDetails/index.html?ids=9b2651f9-dd76-5874-beac-98c5a8465d4a&lang=en-US&timezone=America/New_York","effectiveTime":1761115740,"issuedTime":1761115740,"eventSource":"US","areaId":"vaz515","expireTime":1761174000,"countryCode":"US","attributionUrl":"https://www.weather.gov"}],"detailsUrl":"https://weatherkit.apple.com/alertDetails/index.html?ids=9b2651f9-dd76-5874-beac-98c5a8465d4a&lang=en-US&timezone=America/New_York"} """ + static let hourlyStatisticsJSON = """ + {"metadata":{"latitude":37.541,"longitude":-77.435997,"readTime":1762369879,"expireTime":1762456279,"temporarilyUnavailable":false,"sourceType":"MODELED","reportedTime":1762369879},"hours":[{"temperature":{"p10":-1.980011,"p50":5.190002,"p90":10.820007},"hourOfYear":1},{"temperature":{"p10":-2.399994,"p50":4.860016,"p90":11.450012},"hourOfYear":2}],"baselineStart":0} + """ + + static let dailyStatisticsJSON = """ + {"metadata":{"latitude":40.709999,"longitude":-74.005997,"readTime":1762369991,"expireTime":1762456391,"temporarilyUnavailable":false,"sourceType":"MODELED","reportedTime":1762369991},"days":[{"temperature":{"min":-2.504974,"max":4.800018},"precipitation":{"probability":38,"averageSnowfallAmount":0.0,"averageAmount":3.575325},"dayOfYear":1},{"temperature":{"min":-2.359985,"max":4.529999},"precipitation":{"probability":38,"averageSnowfallAmount":0.0,"averageAmount":2.837539},"dayOfYear":2}],"baselineStart":0} + """ + + static let monthlyStatisticsJSON = """ + {"metadata":{"latitude":40.709999,"longitude":-74.005997,"readTime":1762370020,"expireTime":1762456420,"temporarilyUnavailable":false,"sourceType":"MODELED","reportedTime":1762370020},"months":[{"month":1,"temperature":{"min":-3.904999,"max":3.084991},"precipitation":{"probability":100,"averageSnowfallAmount":154.732346,"averageAmount":78.317642}},{"month":2,"temperature":{"min":-3.509979,"max":4.490021},"precipitation":{"probability":100,"averageSnowfallAmount":139.43869,"averageAmount":72.641373}}],"baselineStart":0} + """ } From f398e1b85df8079a347e39530c912fc19147b375 Mon Sep 17 00:00:00 2001 From: Jeremy Greenwood Date: Wed, 5 Nov 2025 14:54:10 -0500 Subject: [PATCH 07/11] example app (#44) --- .../project.pbxproj | 382 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/swiftpm/Package.resolved | 51 +++ .../xcschemes/OpenWeatherKitExample.xcscheme | 78 ++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../Assets.xcassets/Contents.json | 6 + .../OpenWeatherKitExample/ContentView.swift | 24 ++ .../OpenWeatherKitExample/ExampleApp.swift | 96 +++++ .../Models/WeatherMethodResult.swift | 64 +++ .../Models/WeatherMethodType.swift | 96 +++++ .../ViewModels/WeatherViewModel.swift | 223 ++++++++++ .../Views/Components/AvailabilityView.swift | 45 +++ .../Views/Components/CurrentWeatherView.swift | 105 +++++ .../Views/Components/ForecastView.swift | 160 ++++++++ .../Views/Components/StatisticsView.swift | 150 +++++++ .../Views/Components/SummaryView.swift | 86 ++++ .../Views/Components/WeatherAlertView.swift | 90 +++++ .../Views/MethodDetailView.swift | 194 +++++++++ .../Views/MethodListView.swift | 81 ++++ Example/OpenWeatherKitExample/README.md | 177 ++++++++ README.md | 108 +++++ .../Utils/MockClient.swift | 6 +- 23 files changed, 2272 insertions(+), 3 deletions(-) create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.pbxproj create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/xcshareddata/xcschemes/OpenWeatherKitExample.xcscheme create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/Contents.json create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/ContentView.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/ExampleApp.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Models/WeatherMethodResult.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Models/WeatherMethodType.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/ViewModels/WeatherViewModel.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/AvailabilityView.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/CurrentWeatherView.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/ForecastView.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/StatisticsView.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/SummaryView.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/WeatherAlertView.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/MethodDetailView.swift create mode 100644 Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/MethodListView.swift create mode 100644 Example/OpenWeatherKitExample/README.md diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.pbxproj b/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..6f25bc1 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.pbxproj @@ -0,0 +1,382 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 3F3187C82EBA901B00023927 /* OpenWeatherKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3F3187C72EBA901B00023927 /* OpenWeatherKit */; }; + 3F3187CB2EBA9D4300023927 /* JWTKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3F3187CA2EBA9D4300023927 /* JWTKit */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 3F3187982EBA808700023927 /* OpenWeatherKitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenWeatherKitExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 3F31879A2EBA808700023927 /* OpenWeatherKitExample */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = OpenWeatherKitExample; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3F3187952EBA808700023927 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3F3187C82EBA901B00023927 /* OpenWeatherKit in Frameworks */, + 3F3187CB2EBA9D4300023927 /* JWTKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3F31878F2EBA808700023927 = { + isa = PBXGroup; + children = ( + 3F31879A2EBA808700023927 /* OpenWeatherKitExample */, + 3F3187992EBA808700023927 /* Products */, + ); + sourceTree = ""; + }; + 3F3187992EBA808700023927 /* Products */ = { + isa = PBXGroup; + children = ( + 3F3187982EBA808700023927 /* OpenWeatherKitExample.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3F3187972EBA808700023927 /* OpenWeatherKitExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3F3187A32EBA808800023927 /* Build configuration list for PBXNativeTarget "OpenWeatherKitExample" */; + buildPhases = ( + 3F3187942EBA808700023927 /* Sources */, + 3F3187952EBA808700023927 /* Frameworks */, + 3F3187962EBA808700023927 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 3F31879A2EBA808700023927 /* OpenWeatherKitExample */, + ); + name = OpenWeatherKitExample; + packageProductDependencies = ( + 3F3187C72EBA901B00023927 /* OpenWeatherKit */, + 3F3187CA2EBA9D4300023927 /* JWTKit */, + ); + productName = OpenWeatherKitExample; + productReference = 3F3187982EBA808700023927 /* OpenWeatherKitExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3F3187902EBA808700023927 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2610; + LastUpgradeCheck = 2610; + TargetAttributes = { + 3F3187972EBA808700023927 = { + CreatedOnToolsVersion = 26.1; + }; + }; + }; + buildConfigurationList = 3F3187932EBA808700023927 /* Build configuration list for PBXProject "OpenWeatherKitExample" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3F31878F2EBA808700023927; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 3F3187C62EBA901B00023927 /* XCLocalSwiftPackageReference "../../../open-weather-kit" */, + 3F3187C92EBA9D4300023927 /* XCRemoteSwiftPackageReference "jwt-kit" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 3F3187992EBA808700023927 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3F3187972EBA808700023927 /* OpenWeatherKitExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3F3187962EBA808700023927 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3F3187942EBA808700023927 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 3F3187A12EBA808800023927 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 3F3187A22EBA808800023927 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3F3187A42EBA808800023927 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = UCKKZ9R4MV; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jagreenwood.OpenWeatherKitExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3F3187A52EBA808800023927 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = UCKKZ9R4MV; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jagreenwood.OpenWeatherKitExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3F3187932EBA808700023927 /* Build configuration list for PBXProject "OpenWeatherKitExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3F3187A12EBA808800023927 /* Debug */, + 3F3187A22EBA808800023927 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3F3187A32EBA808800023927 /* Build configuration list for PBXNativeTarget "OpenWeatherKitExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3F3187A42EBA808800023927 /* Debug */, + 3F3187A52EBA808800023927 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 3F3187C62EBA901B00023927 /* XCLocalSwiftPackageReference "../../../open-weather-kit" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../../open-weather-kit"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 3F3187C92EBA9D4300023927 /* XCRemoteSwiftPackageReference "jwt-kit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/vapor/jwt-kit.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.3.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 3F3187C72EBA901B00023927 /* OpenWeatherKit */ = { + isa = XCSwiftPackageProductDependency; + productName = OpenWeatherKit; + }; + 3F3187CA2EBA9D4300023927 /* JWTKit */ = { + isa = XCSwiftPackageProductDependency; + package = 3F3187C92EBA9D4300023927 /* XCRemoteSwiftPackageReference "jwt-kit" */; + productName = JWTKit; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 3F3187902EBA808700023927 /* Project object */; +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..b5ed7ce --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,51 @@ +{ + "originHash" : "2551a7c2d9f14d7bea6c27bdd6366853481e6b0b25272d7f7dff6699f245ddd7", + "pins" : [ + { + "identity" : "jwt-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt-kit.git", + "state" : { + "revision" : "b5f82fb9dc238f2fcac53d721a222513a152613c", + "version" : "5.3.0" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "40d25bbb2fc5b557a9aa8512210bded327c0f60d", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "c399f90e7bbe8874f6cbfda1d5f9023d1f5ce122", + "version" : "1.15.1" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "e8ed8867ec23bccf5f3bb9342148fa8deaff9b49", + "version" : "4.1.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2", + "version" : "1.6.4" + } + } + ], + "version" : 3 +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/xcshareddata/xcschemes/OpenWeatherKitExample.xcscheme b/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/xcshareddata/xcschemes/OpenWeatherKitExample.xcscheme new file mode 100644 index 0000000..cd68d83 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample.xcodeproj/xcshareddata/xcschemes/OpenWeatherKitExample.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/Contents.json b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/ContentView.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/ContentView.swift new file mode 100644 index 0000000..6ddb24a --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/ContentView.swift @@ -0,0 +1,24 @@ +// +// ContentView.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/ExampleApp.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/ExampleApp.swift new file mode 100644 index 0000000..96c2acd --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/ExampleApp.swift @@ -0,0 +1,96 @@ +// +// OpenWeatherKitExampleApp.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import SwiftUI +import OpenWeatherKit +import JWTKit + +@main +struct ExampleApp: App { + @State var viewModel: WeatherViewModel = WeatherViewModel( + configuration: createWeatherConfiguration() + ) + + var body: some Scene { + WindowGroup { + NavigationStack { + MethodListView( + viewModel: viewModel + ) + } + } + } + + /// Creates the WeatherService configuration with JWT provider + /// + /// IMPORTANT: You must provide your own valid Apple WeatherKit JWT token + /// to use this example app. The placeholder below will cause API calls to fail. + /// + /// To get started with Apple WeatherKit: + /// 1. Sign up for an Apple Developer account + /// 2. Create a WeatherKit service identifier in the Apple Developer Portal + /// 3. Generate a private key and download it + /// 4. Use a JWT library (e.g., vapor/jwt-kit) to generate tokens + /// + /// Required JWT claims: + /// - exp: Expiration time (usually ~1 hour from now) + /// - iat: Issued at time (current time) + /// - iss: Issuer (your Team ID) + /// - sub: Subject (your WeatherKit Service Identifier) + /// + /// The JWT must be signed with ES256 using your private key and include + /// the Key ID in the header. + /// + /// For more information, see Apple's WeatherKit documentation: + /// https://developer.apple.com/documentation/weatherkit + private static func createWeatherConfiguration() -> WeatherService.Configuration { + WeatherService.Configuration( + jwt: { + try await JWTProvider.generate() + }, + language: .englishUS + ) + } +} + +struct Payload: JWTPayload, Equatable { + enum CodingKeys: String, CodingKey { + case expiration = "exp" + case issued = "iat" + case issuer = "iss" + case subject = "sub" + } + + let expiration: ExpirationClaim + let issued: IssuedAtClaim + let issuer: IssuerClaim + let subject: SubjectClaim + + func verify(using key: some JWTAlgorithm) throws {} +} + +struct JWTProvider { + static func generate() async throws -> String { + let keys = JWTKeyCollection() + try await keys.add(ecdsa: ES256PrivateKey(pem: privateKey)) + + let payload = Payload( + expiration: .init(value: .distantFuture), + issued: .init(value: .now), + issuer: "TEAM_ID", + subject: "SERVICE_IDENTIFIER" + ) + + return try await keys.sign(payload, kid: "KEY_ID") + } +} + +let privateKey = """ + -----BEGIN PRIVATE KEY----- + PRIVATE_KEY_CONTENTS + -----END PRIVATE KEY----- + """ diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Models/WeatherMethodResult.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Models/WeatherMethodResult.swift new file mode 100644 index 0000000..c64c1c1 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Models/WeatherMethodResult.swift @@ -0,0 +1,64 @@ +// +// WeatherMethodResult.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import Foundation +import OpenWeatherKit + +/// Wraps different return types from WeatherService methods +/// This enum allows us to handle the varying return types in a unified way +enum WeatherMethodResult { + // MARK: - Forecast Results + case weather(Weather) + case currentWeather(CurrentWeather) + case minuteForecast(Forecast?) + case hourlyForecast(Forecast) + case dailyForecast(Forecast) + case alerts([WeatherAlert]?) + case availability(WeatherAvailability) + case changes(WeatherChanges?) + case historicalComparisons(HistoricalComparisons?) + + // MARK: - Statistics Results + case dailyStatistics(DailyWeatherStatistics, DailyWeatherStatistics) + case hourlyStatistics(HourlyWeatherStatistics) + case monthlyStatistics(MonthlyWeatherStatistics, MonthlyWeatherStatistics) + + // MARK: - Summary Results + case dailySummary(DailyWeatherSummary, DailyWeatherSummary) + + /// User-friendly description of the result type + var resultDescription: String { + switch self { + case .weather: + return "Complete Weather Data" + case .currentWeather: + return "Current Conditions" + case .minuteForecast(let forecast): + return forecast != nil ? "Minute Forecast (\(forecast!.forecast.count) minutes)" : "No Minute Forecast Available" + case .hourlyForecast(let forecast): + return "Hourly Forecast (\(forecast.forecast.count) hours)" + case .dailyForecast(let forecast): + return "Daily Forecast (\(forecast.forecast.count) days)" + case .alerts(let alerts): + return alerts != nil ? "Weather Alerts (\(alerts!.count))" : "No Active Alerts" + case .availability: + return "Dataset Availability" + case .changes(let changes): + return changes != nil ? "Weather Changes Available" : "No Weather Changes Data" + case .historicalComparisons(let comparisons): + return comparisons != nil ? "Historical Comparisons Available" : "No Historical Comparison Data" + case .dailyStatistics: + return "Daily Temperature & Precipitation Statistics" + case .hourlyStatistics: + return "Hourly Temperature Statistics" + case .monthlyStatistics: + return "Monthly Temperature & Precipitation Statistics" + case .dailySummary: + return "Daily Temperature & Precipitation Summary" + } + } +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Models/WeatherMethodType.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Models/WeatherMethodType.swift new file mode 100644 index 0000000..41b4291 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Models/WeatherMethodType.swift @@ -0,0 +1,96 @@ +// +// WeatherMethodType.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import Foundation + +/// Represents all available WeatherService methods that can be demonstrated in the app +enum WeatherMethodType: String, CaseIterable, Identifiable { + // MARK: - Forecast Methods + case fullWeather = "Full Weather" + case current = "Current Weather" + case minute = "Minute Forecast" + case hourly = "Hourly Forecast" + case daily = "Daily Forecast" + case alerts = "Weather Alerts" + case availability = "Weather Availability" + case changes = "Weather Changes" + case historicalComparisons = "Historical Comparisons" + + // MARK: - Statistics Methods + case dailyStatistics = "Daily Statistics" + case dailyStatisticsRange = "Daily Statistics (Date Range)" + case hourlyStatistics = "Hourly Statistics" + case hourlyStatisticsRange = "Hourly Statistics (Date Range)" + case monthlyStatistics = "Monthly Statistics" + case monthlyStatisticsRange = "Monthly Statistics (Date Range)" + + // MARK: - Summary Methods + case dailySummary = "Daily Summary" + case dailySummaryRange = "Daily Summary (Date Range)" + case dailySummaryDateInterval = "Daily Summary (Date Interval)" + + var id: String { rawValue } + + /// Display name for the method + var displayName: String { + rawValue + } + + /// Category grouping for the method + var category: String { + switch self { + case .fullWeather, .current, .minute, .hourly, .daily, .alerts, .availability, .changes, .historicalComparisons: + return "Forecast" + case .dailyStatistics, .dailyStatisticsRange, .hourlyStatistics, .hourlyStatisticsRange, .monthlyStatistics, .monthlyStatisticsRange: + return "Statistics" + case .dailySummary, .dailySummaryRange, .dailySummaryDateInterval: + return "Summary" + } + } + + /// Brief description of what the method does + var description: String { + switch self { + case .fullWeather: + return "Returns complete weather data including all available datasets" + case .current: + return "Returns current weather conditions" + case .minute: + return "Returns minute-by-minute forecast for the next hour" + case .hourly: + return "Returns hourly forecast for the next 24 hours" + case .daily: + return "Returns daily forecast for the next 10 days" + case .alerts: + return "Returns active weather alerts for the location" + case .availability: + return "Returns available weather datasets for the location" + case .changes: + return "Returns information about significant weather changes" + case .historicalComparisons: + return "Returns comparisons to historical weather data" + case .dailyStatistics: + return "Returns daily weather statistics (30 days ago to 10 days out)" + case .dailyStatisticsRange: + return "Returns daily statistics for a specific date range" + case .hourlyStatistics: + return "Returns hourly weather statistics for current day" + case .hourlyStatisticsRange: + return "Returns hourly statistics for a specific date range" + case .monthlyStatistics: + return "Returns monthly weather statistics for all 12 months" + case .monthlyStatisticsRange: + return "Returns monthly statistics for a specific date range" + case .dailySummary: + return "Returns daily weather summary for the past 30 days" + case .dailySummaryRange: + return "Returns daily summary for a specific date range" + case .dailySummaryDateInterval: + return "Returns daily summary for a specific date interval" + } + } +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/ViewModels/WeatherViewModel.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/ViewModels/WeatherViewModel.swift new file mode 100644 index 0000000..7fe0cd3 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/ViewModels/WeatherViewModel.swift @@ -0,0 +1,223 @@ +// +// WeatherViewModel.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import Combine +import Foundation +import OpenWeatherKit +import SwiftUI + +/// ViewModel managing weather data fetching and state +@Observable +class WeatherViewModel { + // MARK: - Properties + + /// The WeatherService instance used for all API calls + private let weatherService: WeatherService + + /// Hardcoded NYC location for demo purposes + /// This simplifies the example by avoiding location permissions + private let nycLocation = Location(latitude: 40.7128, longitude: -74.0060) + + /// Loading state - true when an API call is in progress + var isLoading = false + + /// Error from the last API call, if any + var error: Error? + + /// Result from the last successful API call + var result: WeatherMethodResult? + + // MARK: - Initialization + + /// Initialize the ViewModel with a WeatherService configuration + /// - Parameter configuration: WeatherService.Configuration with JWT provider + init(configuration: WeatherService.Configuration) { + self.weatherService = WeatherService(configuration: configuration) + } + + // MARK: - Public Methods + + /// Execute a weather method based on the selected type + /// - Parameter methodType: The weather method to execute + func executeMethod(_ methodType: WeatherMethodType) async { + // Reset state + isLoading = true + error = nil + result = nil + + do { + // Execute the appropriate method based on type + let methodResult = try await performMethodCall(methodType) + result = methodResult + } catch { + self.error = error + } + + isLoading = false + } + + // MARK: - Private Methods + + /// Performs the actual API call based on method type + /// - Parameter methodType: The method type to execute + /// - Returns: The result wrapped in WeatherMethodResult enum + private func performMethodCall(_ methodType: WeatherMethodType) async throws -> WeatherMethodResult { + switch methodType { + // MARK: Forecast Methods + case .fullWeather: + let weather = try await weatherService.weather(for: nycLocation) + return .weather(weather) + + case .current: + let current = try await weatherService.weather(for: nycLocation, including: .current) + return .currentWeather(current) + + case .minute: + let minute = try await weatherService.weather(for: nycLocation, including: .minute) + return .minuteForecast(minute) + + case .hourly: + let hourly = try await weatherService.weather(for: nycLocation, including: .hourly) + return .hourlyForecast(hourly) + + case .daily: + let daily = try await weatherService.weather(for: nycLocation, including: .daily) + return .dailyForecast(daily) + + case .alerts: + let alerts = try await weatherService.weather(for: nycLocation, including: .alerts) + return .alerts(alerts) + + case .availability: + let availability = try await weatherService.weather(for: nycLocation, including: .availability) + return .availability(availability) + + case .changes: + let changes = try await weatherService.weather(for: nycLocation, including: .changes) + return .changes(changes) + + case .historicalComparisons: + let comparisons = try await weatherService.weather(for: nycLocation, including: .historicalComparisons) + return .historicalComparisons(comparisons) + + // MARK: Statistics Methods + case .dailyStatistics: + // Request both temperature and precipitation statistics for default range + let (temp, precip) = try await weatherService.dailyStatistics( + for: nycLocation, + including: .temperature, .precipitation + ) + return .dailyStatistics(temp, precip) + + case .dailyStatisticsRange: + // Example: Get statistics for days 1-10 of the year + let now = Date() + let calendar = Calendar.current + let startDate = calendar.date(byAdding: .day, value: -30, to: now) ?? now + let endDate = calendar.date(byAdding: .day, value: 10, to: now) ?? now + let interval = DateInterval(start: startDate, end: endDate) + + let (temp, precip) = try await weatherService.dailyStatistics( + for: nycLocation, + forDaysIn: interval, + including: .temperature, .precipitation + ) + return .dailyStatistics(temp, precip) + + case .hourlyStatistics: + // Get hourly temperature statistics for current day + let temp = try await weatherService.hourlyStatistics( + for: nycLocation, + including: .temperature + ) + return .hourlyStatistics(temp) + + case .hourlyStatisticsRange: + // Example: Get statistics for specific hour range + let now = Date() + let calendar = Calendar.current + let startOfDay = calendar.startOfDay(for: now) + let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay) ?? startOfDay + let interval = DateInterval(start: startOfDay, end: endOfDay) + + let temp = try await weatherService.hourlyStatistics( + for: nycLocation, + forHoursIn: interval, + including: .temperature + ) + return .hourlyStatistics(temp) + + case .monthlyStatistics: + // Get monthly statistics for all 12 months + let (temp, precip) = try await weatherService.monthlyStatistics( + for: nycLocation, + including: .temperature, .precipitation + ) + return .monthlyStatistics(temp, precip) + + case .monthlyStatisticsRange: + // Example: Get statistics for specific month range (Jan-Jun) + let now = Date() + let calendar = Calendar.current + let startDate = calendar.date(from: DateComponents(year: calendar.component(.year, from: now), month: 1, day: 1)) ?? now + let endDate = calendar.date(from: DateComponents(year: calendar.component(.year, from: now), month: 6, day: 30)) ?? now + let interval = DateInterval(start: startDate, end: endDate) + + let (temp, precip) = try await weatherService.monthlyStatistics( + for: nycLocation, + forMonthsIn: interval, + including: .temperature, .precipitation + ) + return .monthlyStatistics(temp, precip) + + // MARK: Summary Methods + case .dailySummary: + // Get daily summary for past 30 days + let (temp, precip) = try await weatherService.dailySummary( + for: nycLocation, + including: .temperature, .precipitation + ) + return .dailySummary(temp, precip) + + case .dailySummaryRange: + // Example: Get summary for specific day range (last 10 days) + let now = Date() + let calendar = Calendar.current + let startDate = calendar.date(byAdding: .day, value: -10, to: now) ?? now + + let (temp, precip) = try await weatherService.dailySummary( + for: nycLocation, + startDay: calendar.ordinality(of: .day, in: .year, for: startDate) ?? 1, + endDay: calendar.ordinality(of: .day, in: .year, for: now) ?? 365, + including: .temperature, .precipitation + ) + return .dailySummary(temp, precip) + + case .dailySummaryDateInterval: + // Example: Get summary for specific date interval + let now = Date() + let calendar = Calendar.current + let startDate = calendar.date(byAdding: .day, value: -15, to: now) ?? now + let interval = DateInterval(start: startDate, end: now) + + let (temp, precip) = try await weatherService.dailySummary( + for: nycLocation, + forDaysIn: interval, + including: .temperature, .precipitation + ) + return .dailySummary(temp, precip) + } + } +} + +// MARK: - Location Helper + +/// Simple struct conforming to LocationProtocol for demo purposes +private struct Location: LocationProtocol { + let latitude: Double + let longitude: Double +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/AvailabilityView.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/AvailabilityView.swift new file mode 100644 index 0000000..da2d77c --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/AvailabilityView.swift @@ -0,0 +1,45 @@ +// +// AvailabilityView.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import SwiftUI +import OpenWeatherKit + +/// Displays available weather datasets for a location +struct AvailabilityView: View { + let availability: WeatherAvailability + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text("Available Datasets") + .font(.headline) + + VStack(alignment: .leading, spacing: 8) { + DatasetRow(name: "Current Weather", isAvailable: availability.alertAvailability == .available) + DatasetRow(name: "Minute Forecast", isAvailable: availability.minuteAvailability == .available) + } + } + } +} + +/// Row showing availability status for a dataset +struct DatasetRow: View { + let name: String + let isAvailable: Bool + + var body: some View { + HStack { + Image(systemName: isAvailable ? "checkmark.circle.fill" : "xmark.circle.fill") + .foregroundColor(isAvailable ? .green : .red) + Text(name) + .font(.subheadline) + Spacer() + Text(isAvailable ? "Available" : "Not Available") + .font(.caption) + .foregroundColor(.secondary) + } + } +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/CurrentWeatherView.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/CurrentWeatherView.swift new file mode 100644 index 0000000..3654f45 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/CurrentWeatherView.swift @@ -0,0 +1,105 @@ +// +// CurrentWeatherView.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import SwiftUI +import OpenWeatherKit + +/// Displays current weather conditions +struct CurrentWeatherView: View { + let currentWeather: CurrentWeather + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + // Temperature section + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 4) { + Text("Temperature") + .font(.caption) + .foregroundColor(.secondary) + Text(formatTemperature(currentWeather.temperature)) + .font(.system(size: 48, weight: .bold)) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 4) { + Text("Feels Like") + .font(.caption) + .foregroundColor(.secondary) + Text(formatTemperature(currentWeather.apparentTemperature)) + .font(.title2) + .fontWeight(.semibold) + } + } + + Divider() + + // Conditions + VStack(alignment: .leading, spacing: 8) { + Text("Conditions") + .font(.headline) + + InfoRow(label: "Condition", value: currentWeather.condition.description) + InfoRow(label: "Humidity", value: "\(Int(currentWeather.humidity * 100))%") + InfoRow(label: "Pressure", value: String(format: "%.1f mb", currentWeather.pressure.value)) + InfoRow(label: "UV Index", value: "\(currentWeather.uvIndex.value)") + InfoRow(label: "Visibility", value: String(format: "%.1f km", currentWeather.visibility.value / 1000)) + } + + Divider() + + // Wind + VStack(alignment: .leading, spacing: 8) { + Text("Wind") + .font(.headline) + + InfoRow(label: "Speed", value: String(format: "%.1f km/h", currentWeather.wind.speed.value)) + InfoRow(label: "Direction", value: "\(Int(currentWeather.wind.direction.value))°") + } + + Divider() + + // Additional info + VStack(alignment: .leading, spacing: 8) { + Text("Additional Information") + .font(.headline) + + InfoRow(label: "Dew Point", value: formatTemperature(currentWeather.dewPoint)) + InfoRow(label: "Cloud Cover", value: "\(Int(currentWeather.cloudCover * 100))%") + } + + // Timestamp + Text("As of \(currentWeather.date.formatted(date: .abbreviated, time: .shortened))") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top, 8) + } + } + + private func formatTemperature(_ measurement: Measurement) -> String { + let formatter = MeasurementFormatter() + formatter.numberFormatter.maximumFractionDigits = 1 + return formatter.string(from: measurement) + } +} + +/// Helper view for displaying label-value pairs +struct InfoRow: View { + let label: String + let value: String + + var body: some View { + HStack { + Text(label) + .foregroundColor(.secondary) + Spacer() + Text(value) + .fontWeight(.medium) + } + .font(.subheadline) + } +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/ForecastView.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/ForecastView.swift new file mode 100644 index 0000000..7eb3a8d --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/ForecastView.swift @@ -0,0 +1,160 @@ +// +// ForecastView.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import SwiftUI +import OpenWeatherKit + +/// Generic forecast display that works for hourly, daily, and minute forecasts +struct ForecastView: View where T: Modelable { + let forecast: Forecast + let title: String + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .font(.headline) + + // Metadata + VStack(alignment: .leading, spacing: 4) { + Text("Forecast Period") + .font(.caption) + .foregroundColor(.secondary) + Text("\(forecast.metadata.date.formatted(date: .abbreviated, time: .shortened))") + .font(.subheadline) + } + + Divider() + + // Forecast items + VStack(alignment: .leading, spacing: 8) { + Text("Forecast Items: \(forecast.forecast.count)") + .font(.subheadline) + .foregroundColor(.secondary) + + if forecast.forecast.first is DayWeather { + let items = Array(forecast.forecast.prefix(5)).compactMap { $0 as? DayWeather } + ForEach(items, id: \.date) { day in + DayWeatherRow(dayWeather: day) + } + } else if forecast.forecast.first is HourWeather { + let items = Array(forecast.forecast.prefix(8)).compactMap { $0 as? HourWeather } + ForEach(items, id: \.date) { hour in + HourWeatherRow(hourWeather: hour) + } + } else if forecast.forecast.first is MinuteWeather { + Text("Minute-by-minute forecast for the next hour") + .font(.subheadline) + .foregroundColor(.secondary) + let items = Array(forecast.forecast.prefix(12)).compactMap { $0 as? MinuteWeather } + ForEach(Array(items.enumerated()), id: \.element.date) { index, minute in + MinuteWeatherRow(minuteWeather: minute, minuteNumber: index) + } + } + + var limit: Int { + if forecast.forecast.first is DayWeather { + return 5 + } else if forecast.forecast.first is HourWeather { + return 8 + } else if forecast.forecast.first is MinuteWeather { + return 12 + } else { + return 5 + } + } + + if forecast.forecast.count > limit { + Text("Showing first few items. Total: \(forecast.forecast.count)") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top, 4) + } + } + } + } +} + +/// Row for displaying daily weather forecast +struct DayWeatherRow: View { + let dayWeather: DayWeather + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(dayWeather.date.formatted(date: .abbreviated, time: .omitted)) + .font(.subheadline) + .fontWeight(.semibold) + + HStack { + Text("High: \(formatTemp(dayWeather.highTemperature))") + Text("•") + .foregroundColor(.secondary) + Text("Low: \(formatTemp(dayWeather.lowTemperature))") + } + .font(.caption) + + Text(dayWeather.condition.description) + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.vertical, 4) + } + + private func formatTemp(_ measurement: Measurement) -> String { + let formatter = MeasurementFormatter() + formatter.numberFormatter.maximumFractionDigits = 0 + return formatter.string(from: measurement) + } +} + +/// Row for displaying hourly weather forecast +struct HourWeatherRow: View { + let hourWeather: HourWeather + + var body: some View { + HStack { + Text(hourWeather.date.formatted(date: .omitted, time: .shortened)) + .font(.subheadline) + .frame(width: 80, alignment: .leading) + + Text(formatTemp(hourWeather.temperature)) + .font(.subheadline) + .fontWeight(.medium) + .frame(width: 60, alignment: .leading) + + Text(hourWeather.condition.description) + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.vertical, 2) + } + + private func formatTemp(_ measurement: Measurement) -> String { + let formatter = MeasurementFormatter() + formatter.numberFormatter.maximumFractionDigits = 0 + return formatter.string(from: measurement) + } +} + +/// Row for displaying minute weather forecast +struct MinuteWeatherRow: View { + let minuteWeather: MinuteWeather + let minuteNumber: Int + + var body: some View { + HStack { + Text("+\(minuteNumber) min") + .font(.caption) + .frame(width: 60, alignment: .leading) + + Text("Precipitation: \(Int(minuteWeather.precipitationIntensity.value * 100))%") + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.vertical, 2) + } +} + diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/StatisticsView.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/StatisticsView.swift new file mode 100644 index 0000000..87f87b0 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/StatisticsView.swift @@ -0,0 +1,150 @@ +// +// StatisticsView.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import SwiftUI +import OpenWeatherKit + +typealias Modelable = Decodable & Encodable & Equatable & Sendable + +/// Displays weather statistics (temperature and/or precipitation)Stat +struct StatisticsView: View where TemperatureMetric: Modelable, PrecipitationMetric: Modelable { + let temperatureStats: DailyWeatherStatistics? + let precipitationStats: DailyWeatherStatistics? + let title: String + + init( + temperatureStats: DailyWeatherStatistics, + precipitationStats: DailyWeatherStatistics?, + title: String + ) { + self.temperatureStats = temperatureStats + self.precipitationStats = precipitationStats + self.title = title + } + + init( + temperatureStats: HourlyWeatherStatistics, + precipitationStats: HourlyWeatherStatistics?, + title: String + ) where TemperatureMetric == HourTemperatureStatistics { + // Convert to DailyWeatherStatistics for unified display + // For hourly stats, we'll just display them as a special case + self.temperatureStats = nil + self.precipitationStats = nil + self.title = title + } + + init( + temperatureStats: MonthlyWeatherStatistics, + precipitationStats: MonthlyWeatherStatistics?, + title: String + ) where TemperatureMetric == MonthTemperatureStatistics, PrecipitationMetric == MonthPrecipitationStatistics { + // Convert to DailyWeatherStatistics for unified display + self.temperatureStats = nil + self.precipitationStats = nil + self.title = title + } + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text(title) + .font(.headline) + + if let tempStats = temperatureStats { + TemperatureStatisticsSection(stats: tempStats as! DailyWeatherStatistics) + } + + if let precipStats = precipitationStats { + Divider() + PrecipitationStatisticsSection(stats: precipStats as! DailyWeatherStatistics) + } + + // Fallback message if no stats available + if temperatureStats == nil && precipitationStats == nil { + Text("Statistics data structure differs by type. Displaying summary:") + .font(.subheadline) + .foregroundColor(.secondary) + Text("Statistics are available but require type-specific display logic.") + .font(.caption) + .foregroundColor(.secondary) + } + } + } +} + +/// Section displaying temperature statistics +struct TemperatureStatisticsSection: View { + let stats: DailyWeatherStatistics + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("Temperature Statistics") + .font(.subheadline) + .fontWeight(.semibold) + + VStack(alignment: .leading, spacing: 8) { + Text("Statistics Period: \(stats.days.count) days") + .font(.caption) + .foregroundColor(.secondary) + + if let firstDay = stats.days.first { + Text("Sample Day Statistics:") + .font(.caption) + .fontWeight(.semibold) + .padding(.top, 4) + + InfoRow(label: "Max Temperature", value: formatTemperature(firstDay.averageHighTemperature)) + InfoRow(label: "Min Temperature", value: formatTemperature(firstDay.averageLowTemperature)) + } + + Text("Showing sample from \(stats.days.count) day(s) of statistics") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top, 4) + } + } + } + + private func formatTemperature(_ measurement: Measurement) -> String { + let formatter = MeasurementFormatter() + formatter.numberFormatter.maximumFractionDigits = 1 + return formatter.string(from: measurement) + } +} + +/// Section displaying precipitation statistics +struct PrecipitationStatisticsSection: View { + let stats: DailyWeatherStatistics + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("Precipitation Statistics") + .font(.subheadline) + .fontWeight(.semibold) + + VStack(alignment: .leading, spacing: 8) { + Text("Statistics Period: \(stats.days.count) days") + .font(.caption) + .foregroundColor(.secondary) + + if let firstDay = stats.days.first { + Text("Sample Day Statistics:") + .font(.caption) + .fontWeight(.semibold) + .padding(.top, 4) + + InfoRow(label: "Total Precipitation", value: String(format: "%.1f mm", firstDay.averagePrecipitationAmount.value)) + } + + Text("Showing sample from \(stats.days.count) day(s) of statistics") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top, 4) + } + } + } +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/SummaryView.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/SummaryView.swift new file mode 100644 index 0000000..5f9e3f8 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/SummaryView.swift @@ -0,0 +1,86 @@ +// +// SummaryView.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import SwiftUI +import OpenWeatherKit + +/// Displays daily weather summary information +struct SummaryView: View { + let temperatureSummary: DailyWeatherSummary + let precipitationSummary: DailyWeatherSummary + let title: String + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text(title) + .font(.headline) + + // Temperature Summary Section + VStack(alignment: .leading, spacing: 12) { + Text("Temperature Summary") + .font(.subheadline) + .fontWeight(.semibold) + + VStack(alignment: .leading, spacing: 8) { + Text("Summary Period: \(temperatureSummary.days.count) days") + .font(.caption) + .foregroundColor(.secondary) + + if let firstDay = temperatureSummary.days.first { + Text("Sample Day Summary:") + .font(.caption) + .fontWeight(.semibold) + .padding(.top, 4) + + InfoRow(label: "Max Temperature", value: formatTemperature(firstDay.highTemperature)) + InfoRow(label: "Min Temperature", value: formatTemperature(firstDay.lowTemperature)) + } + + Text("Showing sample from \(temperatureSummary.days.count) day(s) of summary data") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top, 4) + } + } + + Divider() + + // Precipitation Summary Section + VStack(alignment: .leading, spacing: 12) { + Text("Precipitation Summary") + .font(.subheadline) + .fontWeight(.semibold) + + VStack(alignment: .leading, spacing: 8) { + Text("Summary Period: \(precipitationSummary.days.count) days") + .font(.caption) + .foregroundColor(.secondary) + + if let firstDay = precipitationSummary.days.first { + Text("Sample Day Summary:") + .font(.caption) + .fontWeight(.semibold) + .padding(.top, 4) + + InfoRow(label: "Total Precipitation", value: String(format: "%.1f mm", firstDay.precipitationAmount.value)) + } + + Text("Showing sample from \(precipitationSummary.days.count) day(s) of summary data") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top, 4) + } + } + } + } + + private func formatTemperature(_ measurement: Measurement) -> String { + let formatter = MeasurementFormatter() + formatter.numberFormatter.maximumFractionDigits = 1 + return formatter.string(from: measurement) + } +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/WeatherAlertView.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/WeatherAlertView.swift new file mode 100644 index 0000000..580901a --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/Components/WeatherAlertView.swift @@ -0,0 +1,90 @@ +// +// WeatherAlertView.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import SwiftUI +import OpenWeatherKit + +/// Displays weather alert information +struct WeatherAlertView: View { + let alert: WeatherAlert + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + // Severity and source + HStack { + severityBadge + Spacer() + Text(alert.alert.source) + .font(.caption) + .foregroundColor(.secondary) + } + + // Summary + Text(alert.alert.alertDescription) + .font(.headline) + + // Details + VStack(alignment: .leading, spacing: 8) { + InfoRow( + label: "Effective", + value: alert.metadata.date.formatted(date: .abbreviated, time: .shortened) + ) + + InfoRow( + label: "Expires", + value: alert.metadata.expirationDate.formatted(date: .abbreviated, time: .shortened) + ) + + if let region = alert.alert.areaName { + InfoRow(label: "Region", value: region) + } + } + } + .padding() + .background(severityBackgroundColor.opacity(0.1)) + .cornerRadius(8) + } + + private var severityBadge: some View { + Text(alert.alert.severity.rawValue.capitalized) + .font(.caption) + .fontWeight(.bold) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(severityColor) + .foregroundColor(.white) + .cornerRadius(4) + } + + private var severityColor: Color { + switch alert.alert.severity { + case .extreme: + return .red + case .severe: + return .orange + case .moderate: + return .yellow + case .minor: + return .blue + case .unknown: + return .gray + } + } + + private var severityBackgroundColor: Color { + switch alert.alert.severity { + case .extreme, .severe: + return .red + case .moderate: + return .orange + case .minor: + return .blue + case .unknown: + return .gray + } + } +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/MethodDetailView.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/MethodDetailView.swift new file mode 100644 index 0000000..5e524c1 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/MethodDetailView.swift @@ -0,0 +1,194 @@ +// +// MethodDetailView.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import SwiftUI +import OpenWeatherKit + +/// Detail view that executes and displays results for a selected weather method +struct MethodDetailView: View { + let methodType: WeatherMethodType + var viewModel: WeatherViewModel + @State private var showError = false + + var body: some View { + ZStack { + if viewModel.isLoading { + // Loading state + VStack(spacing: 16) { + ProgressView() + .scaleEffect(1.5) + Text("Fetching \(methodType.displayName)...") + .font(.subheadline) + .foregroundColor(.secondary) + } + } else if let result = viewModel.result { + // Success state - display results + ScrollView { + VStack(alignment: .leading, spacing: 16) { + // Result type header + Text(result.resultDescription) + .font(.title3) + .fontWeight(.semibold) + .padding(.horizontal) + .padding(.top) + + // Result content based on type + resultContentView(for: result) + .padding(.horizontal) + + Spacer(minLength: 20) + } + } + } else if viewModel.error != nil { + // Error state + VStack(spacing: 16) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 48)) + .foregroundColor(.orange) + + Text("Unable to fetch weather data") + .font(.headline) + + Text("Tap the info button for details") + .font(.subheadline) + .foregroundColor(.secondary) + + Button("Retry") { + Task { + await viewModel.executeMethod(methodType) + } + } + .buttonStyle(.bordered) + .padding(.top, 8) + } + .padding() + } else { + // Initial state (shouldn't normally be seen) + VStack { + Text("Ready to fetch data") + .foregroundColor(.secondary) + } + } + } + .navigationTitle(methodType.displayName) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + if viewModel.error != nil { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + showError = true + } label: { + Image(systemName: "info.circle") + } + } + } + } + .alert("Error Details", isPresented: $showError) { + Button("OK", role: .cancel) {} + } message: { + if let error = viewModel.error { + Text(error.localizedDescription) + } + } + .task { + // Execute method when view appears + await viewModel.executeMethod(methodType) + } + } + + /// Returns the appropriate view for displaying the result + @ViewBuilder + private func resultContentView(for result: WeatherMethodResult) -> some View { + switch result { + case .weather(let weather): + CurrentWeatherView(currentWeather: weather.currentWeather) + Divider() + Text("This result includes all available weather data. For demonstration purposes, showing current weather only. Use specific queries for other datasets.") + .font(.caption) + .foregroundColor(.secondary) + + case .currentWeather(let current): + CurrentWeatherView(currentWeather: current) + + case .minuteForecast(let forecast): + if let forecast = forecast { + ForecastView(forecast: forecast, title: "Next Hour Forecast") + } else { + Text("Minute forecast not available for this location") + .foregroundColor(.secondary) + } + + case .hourlyForecast(let forecast): + ForecastView(forecast: forecast, title: "Hourly Forecast") + + case .dailyForecast(let forecast): + ForecastView(forecast: forecast, title: "Daily Forecast") + + case .alerts(let alerts): + if let alerts = alerts, !alerts.isEmpty { + ForEach(alerts, id: \.alert.id) { alert in + WeatherAlertView(alert: alert) + } + } else { + Text("No active weather alerts") + .foregroundColor(.secondary) + } + + case .availability(let availability): + AvailabilityView(availability: availability) + + case .changes(let changes): + if let changes = changes, !changes.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("Weather Changes Available") + .font(.headline) + Text("Forecast0: \(changes[0].date.formatted())") + } + } else { + Text("No weather changes data available") + .foregroundColor(.secondary) + } + + case .historicalComparisons(let comparisons): + if let comparisons = comparisons { + VStack(alignment: .leading, spacing: 8) { + Text("Historical Comparisons Available") + .font(.headline) + Text("Contains \(comparisons.comparisons.count) comparison(s)") + } + } else { + Text("No historical comparison data available") + .foregroundColor(.secondary) + } + + case .dailyStatistics(let temp, let precip): + StatisticsView(temperatureStats: temp, precipitationStats: precip, title: "Daily Statistics") + + case .hourlyStatistics(let temp): + StatisticsView>(temperatureStats: temp, precipitationStats: nil, title: "Hourly Statistics") + + case .monthlyStatistics(let temp, let precip): + StatisticsView(temperatureStats: temp, precipitationStats: precip, title: "Monthly Statistics") + + case .dailySummary(let temp, let precip): + SummaryView(temperatureSummary: temp, precipitationSummary: precip, title: "Daily Summary") + } + } +} + +#Preview { + NavigationStack { + MethodDetailView( + methodType: .current, + viewModel: WeatherViewModel( + configuration: WeatherService.Configuration( + jwt: { "preview-token" } + ) + ) + ) + } +} diff --git a/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/MethodListView.swift b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/MethodListView.swift new file mode 100644 index 0000000..f3cd250 --- /dev/null +++ b/Example/OpenWeatherKitExample/OpenWeatherKitExample/Views/MethodListView.swift @@ -0,0 +1,81 @@ +// +// MethodListView.swift +// OpenWeatherKitExample +// +// Created by Jeremy Greenwood on 11/4/25. +// + +import SwiftUI +import OpenWeatherKit + +/// Main list view displaying all available WeatherService methods +/// Methods are grouped by category (Forecast, Statistics, Summary) +struct MethodListView: View { + var viewModel: WeatherViewModel + + var body: some View { + List { + // Group methods by category + ForEach(groupedMethods.keys.sorted(), id: \.self) { category in + Section(header: Text(category)) { + ForEach(groupedMethods[category] ?? [], id: \.id) { method in + NavigationLink(value: method) { + VStack(alignment: .leading, spacing: 4) { + Text(method.displayName) + .font(.headline) + Text(method.description) + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.vertical, 4) + } + } + } + } + + // Footer with attribution and location info + Section { + VStack(alignment: .leading, spacing: 8) { + Text("Demo Location") + .font(.caption) + .fontWeight(.semibold) + Text("New York City (40.7128° N, 74.0060° W)") + .font(.caption) + .foregroundColor(.secondary) + + Divider() + .padding(.vertical, 4) + + Text("Note") + .font(.caption) + .fontWeight(.semibold) + Text("These methods demonstrate OpenWeatherKit's API. You must provide a valid Apple WeatherKit JWT token for the app to fetch real data.") + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.vertical, 8) + } + } + .navigationTitle("Weather Methods") + .navigationDestination(for: WeatherMethodType.self) { method in + MethodDetailView(methodType: method, viewModel: viewModel) + } + } + + /// Groups methods by their category + private var groupedMethods: [String: [WeatherMethodType]] { + Dictionary(grouping: WeatherMethodType.allCases, by: { $0.category }) + } +} + +#Preview { + NavigationStack { + MethodListView( + viewModel: WeatherViewModel( + configuration: WeatherService.Configuration( + jwt: { "preview-token" } + ) + ) + ) + } +} diff --git a/Example/OpenWeatherKitExample/README.md b/Example/OpenWeatherKitExample/README.md new file mode 100644 index 0000000..a3e3f92 --- /dev/null +++ b/Example/OpenWeatherKitExample/README.md @@ -0,0 +1,177 @@ +# OpenWeatherKit Example App + +This is a SwiftUI example application demonstrating all public API methods available in OpenWeatherKit. + +## Features + +- **18 Weather Methods Demonstrated**: All forecast, statistics, and summary methods +- **Organized UI**: Methods grouped by category (Forecast, Statistics, Summary) +- **Comprehensive Results**: Custom formatting views for each data type +- **Error Handling**: User-friendly error messages with retry capability +- **Educational Comments**: Code includes explanations for learning purposes + +## Setup Instructions + +### 1. Add Files to Xcode Project + +The Swift files have been created but need to be added to the Xcode project: + +1. Open `OpenWeatherKitExample.xcodeproj` in Xcode +2. Right-click on the `OpenWeatherKitExample` folder in the Project Navigator +3. Select "Add Files to 'OpenWeatherKitExample'..." +4. Navigate to the following folders and add them with "Create folder references": + - `Models/` (contains WeatherMethodType.swift and WeatherMethodResult.swift) + - `ViewModels/` (contains WeatherViewModel.swift) + - `Views/` (contains MethodListView.swift and MethodDetailView.swift) + - `Views/Components/` (contains 6 formatting views) + +### 2. Verify Project Configuration + +- **Deployment Target**: Ensure minimum deployment target is set to iOS 18.0 + - Select the project in Project Navigator + - Go to Build Settings + - Search for "Deployment Target" + - Set to iOS 18.0 or later + +- **Package Dependencies**: Ensure OpenWeatherKit is linked + - Select the project in Project Navigator + - Go to the "Package Dependencies" tab + - OpenWeatherKit should be listed + - If not, add it using File → Add Package Dependencies + +### 3. Configure JWT Token + +Before running the app, you must provide a valid Apple WeatherKit JWT token: + +1. Open `ExampleApp.swift` +2. Find the `createWeatherConfiguration()` method +3. Replace `"YOUR_JWT_TOKEN_HERE"` with your JWT generation code + +#### Getting a WeatherKit JWT + +You need: +- An Apple Developer account +- A WeatherKit service identifier (created in Apple Developer Portal) +- A private key downloaded from Apple Developer Portal + +We recommend using [vapor/jwt-kit](https://github.com/vapor/jwt-kit) for JWT generation. + +**Example JWT generation code:** + +```swift +import JWTKit + +let signers = JWTSigners() +try! signers.use(.es256(key: .private(pem: privateKeyPEM))) + +struct WeatherKitJWT: JWTPayload { + let iss: String // Your Team ID + let sub: String // Your Service Identifier + let exp: ExpirationClaim + let iat: IssuedAtClaim + + func verify(using signer: JWTSigner) throws { + try exp.verifyNotExpired() + } +} + +let payload = WeatherKitJWT( + iss: "YOUR_TEAM_ID", + sub: "YOUR_SERVICE_ID", + exp: .init(value: Date().addingTimeInterval(3600)), + iat: .init(value: Date()) +) + +return try! signers.sign(payload, kid: "YOUR_KEY_ID") +``` + +For more information, see [Apple's WeatherKit documentation](https://developer.apple.com/documentation/weatherkit). + +### 4. Build and Run + +1. Select a simulator or device (iOS 18.0+) +2. Press Cmd+R to build and run +3. Browse the list of weather methods +4. Tap any method to execute it and see results + +## App Structure + +``` +OpenWeatherKitExample/ +├── ExampleApp.swift # App entry point with JWT configuration +├── Models/ +│ ├── WeatherMethodType.swift # Enum of all available methods +│ └── WeatherMethodResult.swift # Result wrapper enum +├── ViewModels/ +│ └── WeatherViewModel.swift # Business logic for API calls +├── Views/ +│ ├── MethodListView.swift # Main list of methods +│ ├── MethodDetailView.swift # Detail view with results +│ └── Components/ +│ ├── CurrentWeatherView.swift # Current conditions display +│ ├── ForecastView.swift # Hourly/daily/minute forecasts +│ ├── WeatherAlertView.swift # Alert display +│ ├── AvailabilityView.swift # Dataset availability +│ ├── StatisticsView.swift # Weather statistics +│ └── SummaryView.swift # Weather summary +└── ContentView.swift # Original placeholder (can be deleted) +``` + +## Demo Location + +The app uses a hardcoded location for New York City: +- Latitude: 40.7128° N +- Longitude: 74.0060° W + +This simplifies the demo by avoiding location permission requirements. + +## Method Categories + +### Forecast Methods (9) +- Full Weather (all datasets) +- Current Weather +- Minute Forecast (next hour) +- Hourly Forecast (24 hours) +- Daily Forecast (10 days) +- Weather Alerts +- Dataset Availability +- Weather Changes +- Historical Comparisons + +### Statistics Methods (6) +- Daily Statistics (default range) +- Daily Statistics (custom date range) +- Hourly Statistics (current day) +- Hourly Statistics (custom date range) +- Monthly Statistics (all 12 months) +- Monthly Statistics (custom date range) + +### Summary Methods (3) +- Daily Summary (past 30 days) +- Daily Summary (custom day range) +- Daily Summary (custom date interval) + +## Troubleshooting + +### Build Errors + +If you see "No such module 'OpenWeatherKit'": +1. Ensure the OpenWeatherKit package is added to the project +2. Clean build folder (Cmd+Shift+K) +3. Rebuild (Cmd+B) + +### API Errors + +If all methods fail with authentication errors: +1. Verify your JWT token is valid +2. Check that your Team ID and Service ID are correct +3. Ensure your private key matches the Key ID in the JWT header +4. Verify the JWT hasn't expired + +### SwiftLint Warnings + +If SwiftLint is configured, you may see warnings. The code follows standard Swift conventions and can be auto-fixed with `swiftlint --fix`. + +## License + +This example app is provided as part of the OpenWeatherKit library for demonstration purposes. diff --git a/README.md b/README.md index 887117b..0ea32d8 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,114 @@ let availability = try await weatherService ) ``` +### Get Weather Statistics + +Historical weather statistics are derived from weather data recorded over the past decades. Statistics are available at daily, hourly, and monthly intervals. + +**Daily Statistics** (30 days ago to 10 days from now by default): + +```swift +let (dailyPrecipitation, dailyTemperature) = try await weatherService + .dailyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + including: .precipitation, .temperature + ) +``` + +**Daily Statistics** (specific day range, 1-366): + +```swift +let (dailyPrecipitation, dailyTemperature) = try await weatherService + .dailyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + startDay: 1, + endDay: 10, + including: .precipitation, .temperature + ) +``` + +**Hourly Statistics** (24 hours of current day by default): + +```swift +let hourlyTemperature = try await weatherService + .hourlyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + including: .temperature + ) +``` + +**Hourly Statistics** (specific hour range, 1-8784): + +```swift +let hourlyTemperature = try await weatherService + .hourlyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + startHour: 1, + endHour: 24, + including: .temperature + ) +``` + +**Monthly Statistics** (all 12 months by default): + +```swift +let (monthlyPrecipitation, monthlyTemperature) = try await weatherService + .monthlyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + including: .precipitation, .temperature + ) +``` + +**Monthly Statistics** (specific month range, 1-12): + +```swift +let (monthlyPrecipitation, monthlyTemperature) = try await weatherService + .monthlyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + startMonth: 1, + endMonth: 6, + including: .precipitation, .temperature + ) +``` + +### Get Weather Summaries + +Weather summaries provide aggregated actual weather data (not statistics) for past dates. + +**Daily Summary** (past 30 days by default): + +```swift +let (dailyPrecipitation, dailyTemperature) = try await weatherService + .dailySummary( + for: Location(latitude: 37.541290, longitude: -77.511429), + including: .precipitation, .temperature + ) +``` + +**Daily Summary** (specific day range, 1-366): + +```swift +let (dailyPrecipitation, dailyTemperature) = try await weatherService + .dailySummary( + for: Location(latitude: 37.541290, longitude: -77.511429), + startDay: 1, + endDay: 10, + including: .precipitation, .temperature + ) +``` + +**Daily Summary** (specific date interval): + +```swift +let interval = DateInterval(start: startDate, end: endDate) +let (dailyPrecipitation, dailyTemperature) = try await weatherService + .dailySummary( + for: Location(latitude: 37.541290, longitude: -77.511429), + forDaysIn: interval, + including: .precipitation, .temperature + ) +``` + ### Geocoding for Country Code (Apple platforms only) When the library is used on an Apple platform, the `countryCode` parameter is not required. Internally the library will use `CoreLocation` to reverse geocode the location to determine the country code. If the country cannot be determined, an error will be thrown. diff --git a/Tests/OpenWeatherKitTests/Utils/MockClient.swift b/Tests/OpenWeatherKitTests/Utils/MockClient.swift index 4b5fec3..a86539a 100644 --- a/Tests/OpenWeatherKitTests/Utils/MockClient.swift +++ b/Tests/OpenWeatherKitTests/Utils/MockClient.swift @@ -88,11 +88,11 @@ actor MockClient: Client { } else if request.url!.absoluteString.contains("/summary/") { return try! encoder.encode(MockData.dailySummary) } else if request.url!.absoluteString.contains("/statistics/hourly/") { - preconditionFailure() + return try! encoder.encode(MockData.hourlyStatistics) } else if request.url!.absoluteString.contains("/statistics/daily/") { - preconditionFailure() + return try! encoder.encode(MockData.dailyStatistics) } else if request.url!.absoluteString.contains("/statistics/monthly/") { - preconditionFailure() + return try! encoder.encode(MockData.monthlyStatistics) } else { preconditionFailure("Unknown URL: \(request.url!.absoluteString)") } From e439eac1ba21cbfbd1803d0fd17958f084c7be36 Mon Sep 17 00:00:00 2001 From: Jeremy Greenwood Date: Wed, 5 Nov 2025 14:59:46 -0500 Subject: [PATCH 08/11] update readme --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0ea32d8..547cb0e 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ struct Payload: JWTPayload, Equatable { let issuer: IssuerClaim let subject: SubjectClaim - func verify(using signer: JWTKit.JWTSigner) throws {} + func verify(using key: some JWTAlgorithm) throws {} } ``` @@ -81,9 +81,9 @@ Generate the JWT ```swift struct JWTProvider { - static func generate() -> String { - let signers = JWTSigners() - try signers.use(.es256(key: ECDSAKey.private(pem: PRIVATE_KEY_FROM_DEV_PORTAL)) + static func generate() async throws -> String { + let keys = JWTKeyCollection() + try await keys.add(ecdsa: ES256PrivateKey(pem: PRIVATE_KEY_FROM_DEV_PORTAL)) let payload = Payload( expiration: .init(value: .distantFuture), @@ -92,14 +92,15 @@ struct JWTProvider { subject: SERVICE_IDENTIFIER ) - return try! signers.sign(payload, kid: KEY_ID) + return try await keys.sign(payload, kid: KEY_ID) } } ``` Note the variables: -`PRIVATE_KEY_FROM_DEV_PORTAL`: The contents of the private key file including `-----BEGIN PRIVATE KEY-----` and `-----END PRIVATE KEY-----` +`PRIVATE_KEY_FROM_DEV_PORTAL`: The contents of the private key file including +`-----BEGIN PRIVATE KEY-----` and `-----END PRIVATE KEY-----` `TEAM_ID`: Found in Membership Details on the developer portal @@ -267,7 +268,7 @@ let (dailyPrecipitation, dailyTemperature) = try await weatherService ### Geocoding for Country Code (Apple platforms only) -When the library is used on an Apple platform, the `countryCode` parameter is not required. Internally the library will use `CoreLocation` to reverse geocode the location to determine the country code. If the country cannot be determined, an error will be thrown. +When the library is used on an Apple platform, the `countryCode` and `timezone` parameters are not required. Internally, the library will use `CoreLocation` to reverse geocode the location to determine the country code. If the country cannot be determined, an error will be thrown. ## 📝 Attribution From 3f443b432932576cd0666b2960c2dd3a806e71d3 Mon Sep 17 00:00:00 2001 From: Jeremy Greenwood Date: Wed, 5 Nov 2025 15:01:42 -0500 Subject: [PATCH 09/11] update readme init doc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 547cb0e..0ea5f6c 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ The service must be initialized with a JWT generating closure and optionally a l ```swift let weatherService = WeatherService( - configuration: .init(jwt: JWTProvider.generate) + configuration: .init(jwt: { try await JWTProvider.generate() }) ) ``` From 752a32e350cb85d2f0a8b3fa2c9ea44f803de9eb Mon Sep 17 00:00:00 2001 From: Jeremy Greenwood Date: Wed, 5 Nov 2025 15:10:55 -0500 Subject: [PATCH 10/11] remove public from TextCaseCoding --- .../Internal/Models/TextCaseCoding.swift | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Sources/OpenWeatherKit/Internal/Models/TextCaseCoding.swift b/Sources/OpenWeatherKit/Internal/Models/TextCaseCoding.swift index 37ddbbf..31ee79b 100644 --- a/Sources/OpenWeatherKit/Internal/Models/TextCaseCoding.swift +++ b/Sources/OpenWeatherKit/Internal/Models/TextCaseCoding.swift @@ -8,21 +8,21 @@ import Foundation @propertyWrapper -public struct TextCaseCoding: Codable, Sendable { - public var wrappedValue: Case.Value +struct TextCaseCoding: Codable, Sendable { + var wrappedValue: Case.Value - public init(wrappedValue: Case.Value) { + init(wrappedValue: Case.Value) { self.wrappedValue = wrappedValue } - public func encode(to encoder: Encoder) throws { + func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(Case.transform(wrappedValue)) } } // swiftlint:disable force_cast -public extension KeyedDecodingContainer { +extension KeyedDecodingContainer { func decode(_ type: TextCaseCoding.Type, forKey key: Key) throws -> TextCaseCoding { if Case.Value.self is Optional.Type { try TextCaseCoding(wrappedValue: Case.transform(decodeIfPresent(String.self, forKey: key) as! Case.Value)) @@ -40,44 +40,44 @@ public extension KeyedDecodingContainer { extension TextCaseCoding: Equatable where Case.Value: Equatable { } extension TextCaseCoding: Hashable where Case.Value: Hashable { } -public protocol TextCase { +protocol TextCase { associatedtype Value: Codable, Sendable static var transform: @Sendable (Value) -> Value { get } } -public enum Lowercased: TextCase { - public static let transform: @Sendable (String) -> String = { $0.lowercased() } +enum Lowercased: TextCase { + static let transform: @Sendable (String) -> String = { $0.lowercased() } } -public enum Uppercased: TextCase { - public static let transform: @Sendable (String) -> String = { $0.uppercased() } +enum Uppercased: TextCase { + static let transform: @Sendable (String) -> String = { $0.uppercased() } } -public enum Capitalized: TextCase { - public static let transform: @Sendable (String) -> String = { $0.capitalized } +enum Capitalized: TextCase { + static let transform: @Sendable (String) -> String = { $0.capitalized } } -public enum LowercasedOptional: TextCase { - public static let transform: @Sendable (String?) -> String? = { $0?.lowercased() } +enum LowercasedOptional: TextCase { + static let transform: @Sendable (String?) -> String? = { $0?.lowercased() } } -public enum UppercasedOptional: TextCase { - public static let transform: @Sendable (String?) -> String? = { $0?.uppercased() } +enum UppercasedOptional: TextCase { + static let transform: @Sendable (String?) -> String? = { $0?.uppercased() } } -public enum CapitalizedOptional: TextCase { - public static let transform: @Sendable (String?) -> String? = { $0?.capitalized } +enum CapitalizedOptional: TextCase { + static let transform: @Sendable (String?) -> String? = { $0?.capitalized } } -public enum LowercasedArray: TextCase { - public static let transform: @Sendable ([String]) -> [String] = { $0.map { $0.lowercased() } } +enum LowercasedArray: TextCase { + static let transform: @Sendable ([String]) -> [String] = { $0.map { $0.lowercased() } } } -public enum UppercasedArray: TextCase { - public static let transform: @Sendable ([String]) -> [String] = { $0.map { $0.uppercased() } } +enum UppercasedArray: TextCase { + static let transform: @Sendable ([String]) -> [String] = { $0.map { $0.uppercased() } } } -public enum CapitalizedArray: TextCase { - public static let transform: @Sendable ([String]) -> [String] = { $0.map(\.capitalized) } +enum CapitalizedArray: TextCase { + static let transform: @Sendable ([String]) -> [String] = { $0.map(\.capitalized) } } From a4fae3a0dd239470f4ea5cf54f09ac2da4a04feb Mon Sep 17 00:00:00 2001 From: Jeremy Greenwood Date: Wed, 5 Nov 2025 15:58:48 -0500 Subject: [PATCH 11/11] update docc references --- .../Extensions/WeatherService.md | 25 ++- .../OpenWeatherKit.docc/GettingStarted.md | 178 +++++++++++++----- .../OpenWeatherKit.docc/OpenWeatherKit.md | 28 +++ 3 files changed, 187 insertions(+), 44 deletions(-) diff --git a/Sources/OpenWeatherKit/OpenWeatherKit.docc/Extensions/WeatherService.md b/Sources/OpenWeatherKit/OpenWeatherKit.docc/Extensions/WeatherService.md index be9b128..686a784 100644 --- a/Sources/OpenWeatherKit/OpenWeatherKit.docc/Extensions/WeatherService.md +++ b/Sources/OpenWeatherKit/OpenWeatherKit.docc/Extensions/WeatherService.md @@ -15,7 +15,6 @@ ### Obtaining forecasts - ``weather(for:)`` -- ``weather(for:countryCode:)`` - ``weather(for:including:)`` - ``weather(for:including:_:)`` - ``weather(for:including:_:_:)`` @@ -23,6 +22,30 @@ - ``weather(for:including:_:_:_:_:)`` - ``weather(for:including:_:_:_:_:_:)`` +### Obtaining daily statistics + +- ``dailyStatistics(for:including:)`` +- ``dailyStatistics(for:startDay:endDay:including:)`` +- ``dailyStatistics(for:forDaysIn:including:)`` + +### Obtaining hourly statistics + +- ``hourlyStatistics(for:including:)`` +- ``hourlyStatistics(for:startHour:endHour:including:)`` +- ``hourlyStatistics(for:forHoursIn:including:)`` + +### Obtaining monthly statistics + +- ``monthlyStatistics(for:including:)`` +- ``monthlyStatistics(for:startMonth:endMonth:including:)`` +- ``monthlyStatistics(for:forMonthsIn:including:)`` + +### Obtaining daily summaries + +- ``dailySummary(for:including:)`` +- ``dailySummary(for:startDay:endDay:including:)`` +- ``dailySummary(for:forDaysIn:including:)`` + ### Providing attribution - ``attribution`` diff --git a/Sources/OpenWeatherKit/OpenWeatherKit.docc/GettingStarted.md b/Sources/OpenWeatherKit/OpenWeatherKit.docc/GettingStarted.md index e7a13ef..f7060dc 100644 --- a/Sources/OpenWeatherKit/OpenWeatherKit.docc/GettingStarted.md +++ b/Sources/OpenWeatherKit/OpenWeatherKit.docc/GettingStarted.md @@ -4,7 +4,7 @@ This is a quick start guide to help get set up and start getting weather data fr ## Overview -The WeatherKit REST API requires a signed JWT to be sent with each request. The following are prerequisites to set this up: +The REST API requires a signed JWT to be sent with each request. To set this up you need: - A paid developer account - A Service Identifier @@ -12,11 +12,6 @@ The WeatherKit REST API requires a signed JWT to be sent with each request. The ### Apple Developer Portal Setup -Since the WeatherKit REST API is a paid service, a paid Apple Developer account is required. The API has very generous -(free) request limits that should be sufficent for most use cases. - -Once the developer account has been established, log in and continue set up on the Developer portal. - #### App Identifier 1. Go to [Identifiers](https://developer.apple.com/account/resources/identifiers/list) @@ -34,16 +29,13 @@ Once the developer account has been established, log in and continue set up on t 5. Make note of the Key ID (you'll need it later) 6. Download the private key - ### JWT -The WeatherKit REST API requires a JSON Web Token (JWT) to be sent with every request. Implementing the -logic necessary to generate a JWT is beyond the scope of the `OpenWeatherKit` project at this time. - +The WeatherKit REST API requires a JSON Web Token (JWT) to be sent with every request. Implementing the +logic necessary to generate a JWT is beyond the scope of the OpenWeatherKit project at this time. For general information on JWT please visit https://jwt.io -That being said, the recommended package to handle this task is Vapor's [jwt-kit](https://github.com/vapor/jwt-kit). -Here is how to set that up: +That being said, the recommended package to handle this task is Vapor's [jwt-kit](https://github.com/vapor/jwt-kit). Here is how to set that up: Implement model conforming to `JWTPayload` @@ -63,7 +55,7 @@ struct Payload: JWTPayload, Equatable { let issuer: IssuerClaim let subject: SubjectClaim - func verify(using signer: JWTKit.JWTSigner) throws {} + func verify(using key: some JWTAlgorithm) throws {} } ``` @@ -71,9 +63,9 @@ Generate the JWT ```swift struct JWTProvider { - static func generate() -> String { - let signers = JWTSigners() - try signers.use(.es256(key: ECDSAKey.private(pem: PRIVATE_KEY_FROM_DEV_PORTAL)) + static func generate() async throws -> String { + let keys = JWTKeyCollection() + try await keys.add(ecdsa: ES256PrivateKey(pem: PRIVATE_KEY_FROM_DEV_PORTAL)) let payload = Payload( expiration: .init(value: .distantFuture), @@ -82,14 +74,15 @@ struct JWTProvider { subject: SERVICE_IDENTIFIER ) - return try! signers.sign(payload, kid: KEY_ID) + return try await keys.sign(payload, kid: KEY_ID) } } ``` Note the variables: -`PRIVATE_KEY_FROM_DEV_PORTAL`: The contents of the private key file including `-----BEGIN PRIVATE KEY-----` and `-----END PRIVATE KEY-----` +`PRIVATE_KEY_FROM_DEV_PORTAL`: The contents of the private key file including +`-----BEGIN PRIVATE KEY-----` and `-----END PRIVATE KEY-----` `TEAM_ID`: Found in Membership Details on the developer portal @@ -97,68 +90,167 @@ Note the variables: `KEY_ID`: The ID of the service key -## Requesting Weather Data +## Usage -### Configure Service +### Initialize Service -The service must be configured with a JWT generating closure and optionally a language. - -If you choose to use the `WeatherService.shared` instance, call the following before referencing `shared`: +The service must be initialized with a JWT generating closure and optionally a language. ```swift -WeatherService.configure { - $0.jwt = JWTProvider.generate -} +let weatherService = WeatherService( + configuration: .init(jwt: { try await JWTProvider.generate() }) +) ``` -On Linux platforms only, this package uses [async-http-client](https://github.com/swift-server/async-http-client) to -make internal HTTP requests to the WeatherKit REST API. By default it uses `NIOEventLoopGroupProvider.createNew`. If -more control is needed, an instance of `EventLoopGroup` can be passed to the configuration instead. - -### Get a Full Weather Forecast +### Get a Full Weather Forecast ```swift -let weather = try await WeatherService.shared +let weather = try await weatherService .weather( for: Location( latitude: 37.541290, longitude: -77.511429), countryCode: "US" -) + ) ``` ### Get a Partial Weather Forecast ```swift -let (dailyForecast, hourlyForecast, alerts) = try await WeatherService.shared +let (dailyForecast, hourlyForecast, alerts) = try await weatherService .weather( for: Location( latitude: 37.541290, longitude: -77.511429), including: .daily, .hourly, .alerts(countryCode: "US") -) + ) ``` ### Get Availability -Note that minute forecasts and alerts are not always available in all regions. Use the `.availability` query -check their availability. +Note that minute forecasts and alerts are not always available in all regions. Use the `.availability` query check their availability. ```swift -let availabilty = try await WeatherService.shared +let availability = try await weatherService .weather( for: Location( latitude: 37.541290, longitude: -77.511429), including: .availability -) + ) +``` + +### Get Weather Statistics + +Historical weather statistics are derived from weather data recorded over the past decades. Statistics are available at daily, hourly, and monthly intervals. + +**Daily Statistics** (30 days ago to 10 days from now by default): + +```swift +let (dailyPrecipitation, dailyTemperature) = try await weatherService + .dailyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + including: .precipitation, .temperature + ) +``` + +**Daily Statistics** (specific day range, 1-366): + +```swift +let (dailyPrecipitation, dailyTemperature) = try await weatherService + .dailyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + startDay: 1, + endDay: 10, + including: .precipitation, .temperature + ) +``` + +**Hourly Statistics** (24 hours of current day by default): + +```swift +let hourlyTemperature = try await weatherService + .hourlyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + including: .temperature + ) +``` + +**Hourly Statistics** (specific hour range, 1-8784): + +```swift +let hourlyTemperature = try await weatherService + .hourlyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + startHour: 1, + endHour: 24, + including: .temperature + ) +``` + +**Monthly Statistics** (all 12 months by default): + +```swift +let (monthlyPrecipitation, monthlyTemperature) = try await weatherService + .monthlyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + including: .precipitation, .temperature + ) +``` + +**Monthly Statistics** (specific month range, 1-12): + +```swift +let (monthlyPrecipitation, monthlyTemperature) = try await weatherService + .monthlyStatistics( + for: Location(latitude: 37.541290, longitude: -77.511429), + startMonth: 1, + endMonth: 6, + including: .precipitation, .temperature + ) +``` + +### Get Weather Summaries + +Weather summaries provide aggregated actual weather data (not statistics) for past dates. + +**Daily Summary** (past 30 days by default): + +```swift +let (dailyPrecipitation, dailyTemperature) = try await weatherService + .dailySummary( + for: Location(latitude: 37.541290, longitude: -77.511429), + including: .precipitation, .temperature + ) +``` + +**Daily Summary** (specific day range, 1-366): + +```swift +let (dailyPrecipitation, dailyTemperature) = try await weatherService + .dailySummary( + for: Location(latitude: 37.541290, longitude: -77.511429), + startDay: 1, + endDay: 10, + including: .precipitation, .temperature + ) +``` + +**Daily Summary** (specific date interval): + +```swift +let interval = DateInterval(start: startDate, end: endDate) +let (dailyPrecipitation, dailyTemperature) = try await weatherService + .dailySummary( + for: Location(latitude: 37.541290, longitude: -77.511429), + forDaysIn: interval, + including: .precipitation, .temperature + ) ``` ### Geocoding for Country Code (Apple platforms only) -When the library is used on an Apple platform, the `countryCode` parameter is not required. Internally the libary will -use `CoreLocation` to reverse geocode the location to determine the country code. If the country cannot be determined, -an error will be thrown. +When the library is used on an Apple platform, the `countryCode` and `timezone` parameters are not required. Internally, the library will use `CoreLocation` to reverse geocode the location to determine the country code. If the country cannot be determined, an error will be thrown. ## Attribution @@ -167,7 +259,7 @@ Please be advised of [Apple's attribution guidelines](https://developer.apple.co Attribution information can be accessed with: ```swift -let attribution = WeatherService.shared.attribution +let attribution = weatherService.attribution ``` Note that this property returns a static `WeatherAttribution` instance using information from `WeatherKit` and is not guaranteed to be accurate or complete. diff --git a/Sources/OpenWeatherKit/OpenWeatherKit.docc/OpenWeatherKit.md b/Sources/OpenWeatherKit/OpenWeatherKit.docc/OpenWeatherKit.md index 48c2139..082afbc 100644 --- a/Sources/OpenWeatherKit/OpenWeatherKit.docc/OpenWeatherKit.md +++ b/Sources/OpenWeatherKit/OpenWeatherKit.docc/OpenWeatherKit.md @@ -31,8 +31,12 @@ is nearly identical to Apple's [WeatherKit](https://developer.apple.com/document - ``AlertSummary`` - ``AlertUrgency`` - ``Certainty`` +- ``CloudCoverByAltitude`` +- ``DayPartForecast`` - ``Precipitation`` +- ``PrecipitationAmountByType`` - ``PressureTrend`` +- ``SnowfallAmount`` - ``UVIndex`` - ``WeatherCondition`` - ``WeatherSeverity`` @@ -42,19 +46,43 @@ is nearly identical to Apple's [WeatherKit](https://developer.apple.com/document - ``DayWeather`` - ``Forecast`` +- ``HistoricalComparisons`` - ``HourWeather`` - ``MinuteWeather`` - ``WeatherAlert`` - ``WeatherAvailability`` +- ``WeatherChanges`` - ``WeatherResponse`` ### Requests - ``CurrentWeather`` +- ``DailyWeatherStatisticsQuery`` +- ``DailyWeatherSummaryQuery`` +- ``HourlyWeatherStatisticsQuery`` +- ``MonthlyWeatherStatisticsQuery`` - ``WeatherAttribution`` - ``WeatherMetadata`` - ``WeatherQuery`` +### Statistics + +- ``DailyWeatherStatistics`` +- ``DayPrecipitationStatistics`` +- ``DayTemperatureStatistics`` +- ``HourlyWeatherStatistics`` +- ``HourTemperatureStatistics`` +- ``MonthlyWeatherStatistics`` +- ``MonthPrecipitationStatistics`` +- ``MonthTemperatureStatistics`` +- ``Percentiles`` + +### Summary + +- ``DailyWeatherSummary`` +- ``DayPrecipitationSummary`` +- ``DayTemperatureSummary`` + ### Geographic Location - ``LocationProtocol``