1#![deny(unsafe_code)]
5pub mod spa;
6
7use crate::spa::{SpaData, SpaError};
8
9pub const SSC_DEFAULT_ATMOSPHERIC_REFRACTION: f64 = 0.5667;
10pub const SSC_DEFAULT_TEMPERATURE: f64 = 16.0;
11pub const SSC_DEFAULT_PRESSURE: f64 = 1013.25;
12pub const SSC_DEFAULT_ELEVATION: f64 = 0.0;
13
14#[derive(Debug, Copy, Clone)]
16pub struct SunriseSunsetParameters {
17 pub time: i64,
19 pub latitude: f64,
21 pub longitude: f64,
23 pub delta_t: f64,
25 pub elevation: f64,
27 pub pressure: f64,
29 pub temperature: f64,
31 pub atmos_refract: f64,
33 pub step_size: u32,
39}
40
41impl SunriseSunsetParameters {
42 pub fn new(time: i64, latitude: f64, longitude: f64) -> Self {
52 SunriseSunsetParameters {
53 time,
54 latitude,
55 longitude,
56 delta_t: 0.0,
57 elevation: SSC_DEFAULT_ELEVATION,
58 pressure: SSC_DEFAULT_PRESSURE,
59 temperature: SSC_DEFAULT_TEMPERATURE,
60 atmos_refract: SSC_DEFAULT_ATMOSPHERIC_REFRACTION,
61 step_size: Self::default_step_size(latitude),
62 }
63 }
64
65 pub fn default_step_size(latitude: f64) -> u32 {
78 let latitude_abs = latitude.abs();
79 if latitude_abs < 60.0 {
80 14400
81 } else if latitude_abs < 64.0 {
82 3600
83 } else {
84 600
85 }
86 }
87
88 pub fn calculate(self) -> Result<SunriseSunsetResult, SpaError> {
90 let mut data = SpaData {
91 jd: jd_from_unix(self.time),
92 delta_t: self.delta_t,
93 longitude: self.longitude,
94 latitude: self.latitude,
95 elevation: self.elevation,
96 pressure: self.pressure,
97 temperature: self.temperature,
98 atmos_refract: self.atmos_refract,
99 ..Default::default()
100 };
101 data.calculate()?;
102
103 let visible = sun_is_up(&data);
104 let step_signed = self.step_size as i64;
105
106 let backward_result =
107 search_for_change_in_visibility(&mut data, self.time, -step_signed, visible)?;
108
109 let forward_result =
110 search_for_change_in_visibility(&mut data, self.time, step_signed, visible)?;
111
112 Ok(SunriseSunsetResult {
113 set: if visible {
114 forward_result
115 } else {
116 backward_result
117 },
118 rise: if visible {
119 backward_result
120 } else {
121 forward_result
122 },
123 visible,
124 })
125 }
126}
127
128#[derive(Debug, Copy, Clone, Eq, PartialEq)]
130pub struct SunriseSunsetResult {
131 pub set: i64,
133 pub rise: i64,
135 pub visible: bool,
137}
138
139#[inline]
140fn jd_from_unix(t: i64) -> f64 {
141 t as f64 / 86400.0f64 + 2440587.5f64
142}
143
144#[inline]
145fn sun_is_up(result: &SpaData) -> bool {
146 result.e >= -0.8333f64
147}
148
149#[inline]
150fn search_for_change_in_visibility(
151 mut data: &mut SpaData,
152 mut start: i64,
153 mut step_size: i64,
154 mut currently_visible: bool,
155) -> Result<i64, SpaError> {
156 while step_size != 0 {
157 data.jd = jd_from_unix(start);
158 data.calculate()?;
159 if sun_is_up(data) != currently_visible {
160 step_size = -(step_size / 2i64);
161 currently_visible = !currently_visible;
162 } else {
163 start += step_size;
164 }
165 }
166 Ok(start)
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use approx::assert_abs_diff_eq;
173 use chrono::{FixedOffset, TimeZone};
174
175 const ACCURACY_SECONDS: i64 = 60;
176
177 const BRISTOL_LAT: f64 = 51.4545;
178 const BRISTOL_LON: f64 = -2.5879;
179
180 const STLOUIS_LAT: f64 = 38.6272;
181 const STLOUIS_LON: f64 = -90.1978;
182
183 const SVALBARD_LAT: f64 = 79.0;
184 const SVALBARD_LON: f64 = 17.0;
185
186 const ADELAIDE_LAT: f64 = -34.92;
187 const ADELAIDE_LON: f64 = 138.59;
188
189 fn timestamp(year: i32, month: u32, day: u32, hour: u32, min: u32, tz_seconds: i32) -> i64 {
190 let tz = FixedOffset::east_opt(tz_seconds).unwrap();
191 tz.with_ymd_and_hms(year, month, day, hour, min, 0)
192 .unwrap()
193 .timestamp()
194 }
195
196 fn validate_result(
197 res: &SunriseSunsetResult,
198 expected_rise: i64,
199 expected_set: i64,
200 currently_visible: bool,
201 ) {
202 assert_abs_diff_eq!(res.rise, expected_rise, epsilon = ACCURACY_SECONDS);
203 assert_abs_diff_eq!(res.set, expected_set, epsilon = ACCURACY_SECONDS);
204 assert_eq!(
205 res.visible, currently_visible,
206 "Solar visibility did not match"
207 );
208 }
209
210 fn calculate(timestamp: i64, latitude: f64, longitude: f64) -> SunriseSunsetResult {
211 SunriseSunsetParameters::new(timestamp, latitude, longitude)
212 .calculate()
213 .unwrap()
214 }
215
216 #[test]
217 fn test_bristol() {
218 let early19 = timestamp(2018, 11, 19, 5, 30, 0);
219 let res = calculate(early19, BRISTOL_LAT, BRISTOL_LON);
220 let set18 = timestamp(2018, 11, 18, 16, 17, 0);
221 let rise19 = timestamp(2018, 11, 19, 7, 35, 0);
222 validate_result(&res, rise19, set18, false);
223
224 let midday19 = timestamp(2018, 11, 19, 12, 0, 0);
225 let res = calculate(midday19, BRISTOL_LAT, BRISTOL_LON);
226 let set19 = timestamp(2018, 11, 19, 16, 16, 0);
227 validate_result(&res, rise19, set19, true);
228
229 let late19 = timestamp(2018, 11, 19, 20, 25, 0);
230 let rise20 = timestamp(2018, 11, 20, 7, 36, 0);
231 let res = calculate(late19, BRISTOL_LAT, BRISTOL_LON);
232 validate_result(&res, rise20, set19, false);
233 }
234
235 fn test_outer_bounds_impl(
236 mut start: i64,
237 latitude: f64,
238 longitude: f64,
239 days: u32,
240 increment: i64,
241 ) {
242 let end = start + (days as i64 * 24 * 60 * 60);
243 let mut params = SunriseSunsetParameters::new(start, latitude, longitude);
244 while start <= end {
245 params.time = start;
246 let res = params.calculate().unwrap();
247 assert!(
248 (res.rise <= start && start <= res.set) || (res.set <= start && start <= res.rise),
249 "Sunrise and Sunset are not on either side of the input time"
250 );
251 assert_eq!((start > res.rise), res.visible, "Visibility incorrect");
252 start += increment;
253 }
254 }
255
256 #[test]
257 fn test_outer_bounds() {
258 let start = timestamp(2021, 07, 28, 10, 0, 0);
259 test_outer_bounds_impl(start, STLOUIS_LAT, STLOUIS_LON, 10, 3600);
260 test_outer_bounds_impl(start, BRISTOL_LAT, BRISTOL_LON, 10, 3600);
261
262 let svalbard_summer = timestamp(2021, 07, 1, 0, 0, 0);
263 test_outer_bounds_impl(svalbard_summer, SVALBARD_LAT, SVALBARD_LON, 5, 3600);
264
265 let svalbard_winter = timestamp(2021, 01, 1, 0, 0, 0);
266 test_outer_bounds_impl(svalbard_winter, SVALBARD_LAT, SVALBARD_LON, 5, 3600);
267
268 let svalbard_spring = timestamp(2021, 02, 16, 0, 0, 0);
269 test_outer_bounds_impl(svalbard_spring, SVALBARD_LAT, SVALBARD_LON, 10, 3600);
270 }
271
272 #[test]
273 fn test_adelaide() {
274 let tz = (10.5 * 60.0 * 60.0) as i32; let mid = timestamp(2021, 11, 13, 19, 00, tz);
276 let res = calculate(mid, ADELAIDE_LAT, ADELAIDE_LON);
277 let set = timestamp(2021, 11, 13, 19, 57, tz);
278 let rise = timestamp(2021, 11, 13, 6, 03, tz);
279 validate_result(&res, rise, set, true);
280 }
281}