1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
import Foundation
class SunTimes {
let latitude: Double
let longitude: Double
let date: Date
init(latitude: Double, longitude: Double, date: Date) {
self.latitude = latitude
self.longitude = longitude
self.date = date
}
/// Calculate sunrise time for the location and date
func sunrise() -> Date? {
guard let result = calculateSunTimes() else { return nil }
return result.sunrise
}
/// Calculate sunset time for the location and date
func sunset() -> Date? {
guard let result = calculateSunTimes() else { return nil }
return result.sunset
}
// NOAA solar position algorithm
// Reference: https://www.esrl.noaa.gov/gmd/grad/solcalc/
private func calculateSunTimes() -> (sunrise: Date, sunset: Date)? {
let calendar = Calendar(identifier: .gregorian)
let components = calendar.dateComponents([.year, .month, .day], from: date)
guard let year = components.year else {
return nil
}
// Step 1: Calculate day of year
let jan1 = calendar.date(from: DateComponents(year: year, month: 1, day: 1))!
let dayOfYear = calendar.dateComponents([.day], from: jan1, to: date).day! + 1
// Step 2: Fractional year in radians
let daysInYear = Double(isLeapYear(year) ? 366 : 365)
let gamma = 2.0 * Double.pi * Double(dayOfYear - 1) / daysInYear
// Step 3: Solar declination (radians)
let decl = 0.006918 - 0.399912 * cos(gamma) + 0.070257 * sin(gamma)
- 0.006758 * cos(2.0 * gamma) + 0.000907 * sin(2.0 * gamma)
- 0.002697 * cos(3.0 * gamma) + 0.00148 * sin(3.0 * gamma)
// Step 4: Equation of time (minutes)
let eot = 229.18 * (0.000075 + 0.001868 * cos(gamma) - 0.032077 * sin(gamma)
- 0.014615 * cos(2.0 * gamma) - 0.040849 * sin(2.0 * gamma))
// Step 5: Hour angle at sunrise/sunset
let latRad = latitude * Double.pi / 180.0
let cosH = -tan(latRad) * tan(decl)
guard cosH >= -1.0 && cosH <= 1.0 else {
// Sun is always up or always down at this latitude/date
return nil
}
let h = acos(cosH) * 180.0 / Double.pi
// Step 6: Solar noon in UTC (decimal hours)
let solarNoonUTC = 12.0 - (longitude / 15.0) - (eot / 60.0)
// Step 7: Sunrise and sunset in UTC (decimal hours)
let sunriseUTC = solarNoonUTC - (h / 15.0)
let sunsetUTC = solarNoonUTC + (h / 15.0)
// Step 8: Convert to local time (add timezone offset)
let tzOffset = Double(TimeZone.current.secondsFromGMT(for: date)) / 3600.0
let sunriseLocal = sunriseUTC + tzOffset
let sunsetLocal = sunsetUTC + tzOffset
// Step 9: Create date components, handling day boundary crossing
let calendar2 = Calendar.current
var baseComponents = calendar2.dateComponents([.year, .month, .day], from: date)
// Sunrise
var sunriseComp = baseComponents
let srHourDouble = sunriseLocal
let srHour = Int(srHourDouble)
let srMinuteDouble = (srHourDouble - Double(srHour)) * 60.0
let srMinute = Int(srMinuteDouble)
if srHour < 0 {
if let prevDay = calendar2.date(byAdding: .day, value: -1, to: date) {
let prevComponents = calendar2.dateComponents([.year, .month, .day], from: prevDay)
sunriseComp = prevComponents
sunriseComp.hour = 24 + srHour
} else {
sunriseComp.hour = 0
}
} else if srHour >= 24 {
if let nextDay = calendar2.date(byAdding: .day, value: 1, to: date) {
let nextComponents = calendar2.dateComponents([.year, .month, .day], from: nextDay)
sunriseComp = nextComponents
sunriseComp.hour = srHour - 24
} else {
sunriseComp.hour = 23
}
} else {
sunriseComp.hour = srHour
}
sunriseComp.minute = max(0, min(59, srMinute))
sunriseComp.second = 0
// Sunset
var sunsetComp = baseComponents
let ssHourDouble = sunsetLocal
let ssHour = Int(ssHourDouble)
let ssMinuteDouble = (ssHourDouble - Double(ssHour)) * 60.0
let ssMinute = Int(ssMinuteDouble)
if ssHour < 0 {
if let prevDay = calendar2.date(byAdding: .day, value: -1, to: date) {
let prevComponents = calendar2.dateComponents([.year, .month, .day], from: prevDay)
sunsetComp = prevComponents
sunsetComp.hour = 24 + ssHour
} else {
sunsetComp.hour = 0
}
} else if ssHour >= 24 {
if let nextDay = calendar2.date(byAdding: .day, value: 1, to: date) {
let nextComponents = calendar2.dateComponents([.year, .month, .day], from: nextDay)
sunsetComp = nextComponents
sunsetComp.hour = ssHour - 24
} else {
sunsetComp.hour = 23
}
} else {
sunsetComp.hour = ssHour
}
sunsetComp.minute = max(0, min(59, ssMinute))
sunsetComp.second = 0
guard let sunriseDate = calendar2.date(from: sunriseComp),
let sunsetDate = calendar2.date(from: sunsetComp) else {
return nil
}
return (sunriseDate, sunsetDate)
}
private func isLeapYear(_ year: Int) -> Bool {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
}
|