Skip to content

Commit c6dc88c

Browse files
committed
add internal models and helpers
1 parent 86f4526 commit c6dc88c

11 files changed

Lines changed: 570 additions & 23 deletions
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// APIDailyStatistics+Map.swift
3+
//
4+
//
5+
// Created by Jeremy Greenwood on 10/29/25.
6+
//
7+
8+
import Foundation
9+
10+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
11+
extension APIDailyStatistics {
12+
var dailyPrecipitationStatistics: DailyWeatherStatistics<DayPrecipitationStatistics> {
13+
DailyWeatherStatistics<DayPrecipitationStatistics>(
14+
days: days.map(\.precipitationStatistics),
15+
baselineStartDate: Date.daysFromEpoch(baselineStart),
16+
metadata: metadata.weatherMetadata
17+
)
18+
}
19+
20+
var dailyTemperatureStatistics: DailyWeatherStatistics<DayTemperatureStatistics> {
21+
DailyWeatherStatistics<DayTemperatureStatistics>(
22+
days: days.map(\.temperatureStatistics),
23+
baselineStartDate: Date.daysFromEpoch(baselineStart),
24+
metadata: metadata.weatherMetadata
25+
)
26+
}
27+
28+
@usableFromInline
29+
func parse<T>(query: DailyWeatherStatisticsQuery<T>) -> DailyWeatherStatistics<T> {
30+
switch query.statisticsType {
31+
case .temperature:
32+
guard let stats = dailyTemperatureStatistics as? DailyWeatherStatistics<T> else {
33+
preconditionFailure("Type mismatch: expected DailyWeatherStatistics<DayTemperatureStatistics> but got DailyWeatherStatistics<\(T.self)>")
34+
}
35+
return stats
36+
case .precipitation:
37+
guard let stats = dailyPrecipitationStatistics as? DailyWeatherStatistics<T> else {
38+
preconditionFailure("Type mismatch: expected DailyWeatherStatistics<DayPrecipitationStatistics> but got DailyWeatherStatistics<\(T.self)>")
39+
}
40+
return stats
41+
}
42+
}
43+
}
44+
45+
@available(iOS 18.0, *)
46+
extension APIDailyStatisticsData {
47+
var precipitationStatistics: DayPrecipitationStatistics {
48+
guard let precipitation else {
49+
preconditionFailure("Precipitation is nil, this could be a logic error")
50+
}
51+
52+
return DayPrecipitationStatistics(
53+
day: dayOfYear,
54+
averagePrecipitationProbability: Double(precipitation.probability),
55+
averagePrecipitationAmount: Measurement(value: precipitation.averageAmount, unit: .millimeters),
56+
averageSnowfallAmount: Measurement(value: precipitation.averageSnowfallAmount, unit: .millimeters)
57+
)
58+
}
59+
60+
var temperatureStatistics: DayTemperatureStatistics {
61+
guard let temperature else {
62+
preconditionFailure("Temperature is nil, this could be a logic error")
63+
}
64+
65+
return DayTemperatureStatistics(
66+
day: dayOfYear,
67+
averageLowTemperature: Measurement(value: temperature.min ?? .nan, unit: .celsius),
68+
averageHighTemperature: Measurement(value: temperature.max ?? .nan, unit: .celsius)
69+
)
70+
}
71+
}
72+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// APIDailySummary+Map.swift
3+
//
4+
//
5+
// Created by Jeremy Greenwood on 10/29/25.
6+
//
7+
8+
import Foundation
9+
10+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
11+
extension APIDailySummary {
12+
var dailyPrecipitationSummary: DailyWeatherSummary<DayPrecipitationSummary> {
13+
DailyWeatherSummary<DayPrecipitationSummary>(
14+
days: days.map(\.precipitationSummary),
15+
metadata: metadata.weatherMetadata
16+
)
17+
}
18+
19+
var dailyTemperatureSummary: DailyWeatherSummary<DayTemperatureSummary> {
20+
DailyWeatherSummary(
21+
days: days.map(\.temperatureSummary),
22+
metadata: metadata.weatherMetadata
23+
)
24+
}
25+
26+
@usableFromInline
27+
func parse<T>(query: DailyWeatherSummaryQuery<T>) -> DailyWeatherSummary<T> {
28+
switch query.statisticsType {
29+
case .temperature:
30+
guard let stats = dailyTemperatureSummary as? DailyWeatherSummary<T> else {
31+
preconditionFailure("Type mismatch: expected DailyWeatherSummary<DayTemperatureSummary> but got DailyWeatherSummary<\(T.self)>")
32+
}
33+
return stats
34+
case .precipitation:
35+
guard let stats = dailyPrecipitationSummary as? DailyWeatherSummary<T> else {
36+
preconditionFailure("Type mismatch: expected DailyWeatherSummary<DayPrecipitationSummary> but got DailyWeatherSummary<\(T.self)>")
37+
}
38+
return stats
39+
}
40+
}
41+
}
42+
43+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
44+
extension APIDailySummaryDay {
45+
var precipitationSummary: DayPrecipitationSummary {
46+
DayPrecipitationSummary(
47+
date: Date.daysFromEpoch(date),
48+
precipitationAmount: Measurement(value: precipitationAmount, unit: .millimeters),
49+
snowfallAmount: Measurement(value: snowfallAmount, unit: .millimeters)
50+
)
51+
}
52+
53+
var temperatureSummary: DayTemperatureSummary {
54+
DayTemperatureSummary(
55+
date: Date.daysFromEpoch(date),
56+
lowTemperature: Measurement(value: temperatureMin, unit: .celsius),
57+
highTemperature: Measurement(value: temperatureMax, unit: .celsius)
58+
)
59+
}
60+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// APIHourlyStatistics+Map.swift
3+
//
4+
//
5+
// Created by Jeremy Greenwood on 10/29/25.
6+
//
7+
8+
import Foundation
9+
10+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
11+
extension APIHourlyStatistics {
12+
@usableFromInline
13+
var hourlyTemperatureStatistics: HourlyWeatherStatistics<HourTemperatureStatistics> {
14+
HourlyWeatherStatistics<HourTemperatureStatistics>(
15+
hours: hours.map(\.temperatureStatistics),
16+
baselineStartDate: Date.daysFromEpoch(baselineStart),
17+
metadata: metadata.weatherMetadata
18+
)
19+
}
20+
21+
@usableFromInline
22+
func parse<T>(query: HourlyWeatherStatisticsQuery<T>) -> HourlyWeatherStatistics<T> {
23+
switch query.statisticsType {
24+
case .temperature:
25+
guard let stats = hourlyTemperatureStatistics as? HourlyWeatherStatistics<T> else {
26+
preconditionFailure("Type mismatch: expected HourlyWeatherStatistics<HourTemperatureStatistics> but got HourlyWeatherStatistics<\(T.self)>")
27+
}
28+
return stats
29+
default:
30+
preconditionFailure("Invalid statistics type: HourlyWeatherStatistics<\(T.self)>")
31+
}
32+
}
33+
}
34+
35+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
36+
extension APIHourlyStatisticsData {
37+
@usableFromInline
38+
var temperatureStatistics: HourTemperatureStatistics {
39+
HourTemperatureStatistics(
40+
hour: hourOfYear,
41+
percentiles: Percentiles(
42+
p10: Measurement(value: temperature.p10 ?? 0.0, unit: .celsius),
43+
p50: Measurement(value: temperature.p50 ?? 0.0, unit: .celsius),
44+
p90: Measurement(value: temperature.p90 ?? 0.0, unit: .celsius)
45+
)
46+
)
47+
}
48+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//
2+
// APIMonthlyStatistics+Map.swift
3+
//
4+
//
5+
// Created by Jeremy Greenwood on 10/29/25.
6+
//
7+
8+
import Foundation
9+
10+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
11+
extension APIMonthlyStatistics {
12+
var monthlyPrecipitationStatistics: MonthlyWeatherStatistics<MonthPrecipitationStatistics> {
13+
MonthlyWeatherStatistics(
14+
months: months.map(\.precipitationStatistics),
15+
baselineStartDate: Date.daysFromEpoch(baselineStart),
16+
metadata: metadata.weatherMetadata
17+
)
18+
}
19+
20+
var monthlyTemperatureStatistics: MonthlyWeatherStatistics<MonthTemperatureStatistics> {
21+
MonthlyWeatherStatistics(
22+
months: months.map(\.temperatureStatistics),
23+
baselineStartDate: Date.daysFromEpoch(baselineStart),
24+
metadata: metadata.weatherMetadata
25+
)
26+
}
27+
28+
@usableFromInline
29+
func parse<T>(query: MonthlyWeatherStatisticsQuery<T>) -> MonthlyWeatherStatistics<T> {
30+
switch query.statisticsType {
31+
case .temperature:
32+
guard let stats = monthlyTemperatureStatistics as? MonthlyWeatherStatistics<T> else {
33+
preconditionFailure("Type mismatch: expected MonthlyWeatherStatistics<MonthTemperatureStatistics> but got MonthlyWeatherStatistics<\(T.self)>")
34+
}
35+
return stats
36+
case .precipitation:
37+
guard let stats = monthlyPrecipitationStatistics as? MonthlyWeatherStatistics<T> else {
38+
preconditionFailure("Type mismatch: expected MonthlyWeatherStatistics<MonthPrecipitationStatistics> but got MonthlyWeatherStatistics<\(T.self)>")
39+
}
40+
return stats
41+
}
42+
}
43+
}
44+
45+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
46+
extension APIMonthlyStatisticsData {
47+
var precipitationStatistics: MonthPrecipitationStatistics {
48+
guard let precipitation else {
49+
preconditionFailure("Precipitation is nil, this could be a logic error")
50+
}
51+
52+
return MonthPrecipitationStatistics(
53+
month: month,
54+
averagePrecipitationProbability: Double(precipitation.probability),
55+
averagePrecipitationAmount: Measurement(value: precipitation.averageAmount, unit: .millimeters),
56+
averageSnowfallAmount: Measurement(value: precipitation.averageSnowfallAmount, unit: .millimeters)
57+
)
58+
}
59+
60+
var temperatureStatistics: MonthTemperatureStatistics {
61+
guard let temperature else {
62+
preconditionFailure("Temperature is nil, this could be a logic error")
63+
}
64+
65+
return MonthTemperatureStatistics(
66+
month: month,
67+
averageLowTemperature: Measurement(value: temperature.min ?? .nan, unit: .celsius),
68+
averageHighTemperature: Measurement(value: temperature.max ?? .nan, unit: .celsius)
69+
)
70+
}
71+
}

Sources/OpenWeatherKit/Internal/Extensions/Date+Utils.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,13 @@ extension Date {
1919
components.hour = hours
2020
return Calendar.current.date(byAdding: components, to: Date())!
2121
}
22+
23+
static func daysFromEpoch(_ days: Int) -> Date {
24+
// Create a date from the Unix epoch (1970-01-01)
25+
let epoch = Date(timeIntervalSince1970: 0)
26+
27+
// Convert days to TimeInterval (seconds) and subtract 1 day since 1970-01-01 is day 1
28+
let timeInterval = TimeInterval((days - 1) * 24 * 60 * 60)
29+
return Date(timeInterval: timeInterval, since: epoch)
30+
}
2231
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
3+
extension DateFormatter {
4+
/// A DateFormatter configured for API date strings in YYYY-MM-DD format.
5+
/// Uses UTC timezone to avoid DST edge cases.
6+
@usableFromInline
7+
static let dateOnly: DateFormatter = {
8+
let formatter = DateFormatter()
9+
formatter.dateFormat = "yyyy-MM-dd"
10+
formatter.timeZone = TimeZone(identifier: "UTC")
11+
formatter.locale = Locale(identifier: "en_US_POSIX")
12+
return formatter
13+
}()
14+
}
15+
16+
extension Date {
17+
/// Converts a Date to a YYYY-MM-DD string in UTC.
18+
@usableFromInline
19+
func toDateString() -> String {
20+
DateFormatter.dateOnly.string(from: self)
21+
}
22+
23+
/// Creates a Date from a YYYY-MM-DD string in UTC.
24+
/// - Parameter dateString: String in YYYY-MM-DD format
25+
/// - Returns: Date object or nil if parsing fails
26+
@usableFromInline
27+
static func fromDateString(_ dateString: String) -> Date? {
28+
DateFormatter.dateOnly.date(from: dateString)
29+
}
30+
}

Sources/OpenWeatherKit/Internal/Extensions/Sendable+Compat.swift

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// APIDailySummary.swift
3+
//
4+
//
5+
// Created by Jeremy Greenwood on 10/29/25.
6+
//
7+
8+
import Foundation
9+
10+
// MARK: - APIDailySummary
11+
@usableFromInline
12+
struct APIDailySummary: Codable, Sendable, Equatable {
13+
let metadata: APIMetadata
14+
let days: [APIDailySummaryDay]
15+
let startDate: Int // Days from epoch
16+
let endDate: Int // Days from epoch
17+
18+
enum CodingKeys: String, CodingKey {
19+
case metadata
20+
case days
21+
case startDate
22+
case endDate
23+
}
24+
}
25+
26+
// MARK: - APIDailySummaryDay
27+
@usableFromInline
28+
struct APIDailySummaryDay: Codable, Sendable, Equatable {
29+
let date: Int // Days from epoch (e.g., days since 1970-01-01)
30+
let temperatureMin: Double
31+
let temperatureMax: Double
32+
let precipitationAmount: Double
33+
let snowfallAmount: Double
34+
35+
enum CodingKeys: String, CodingKey {
36+
case date
37+
case temperatureMin
38+
case temperatureMax
39+
case precipitationAmount
40+
case snowfallAmount
41+
}
42+
}

0 commit comments

Comments
 (0)