1#![allow(clippy::similar_names)]
7#![allow(clippy::many_single_char_names)]
8#![allow(clippy::unreadable_literal)]
9
10use crate::error::{check_coordinates, check_refraction_params_usable};
11use crate::math::{
12 acos, asin, atan, atan2, cos, degrees_to_radians, floor, normalize_degrees_0_to_360,
13 polynomial, radians_to_degrees, sin, tan,
14};
15use crate::time::JulianDate;
16use crate::{Error, Horizon, Result, SolarPosition};
17use chrono::{DateTime, TimeZone};
18
19pub mod coefficients;
20use coefficients::{
21 NUTATION_COEFFS, OBLIQUITY_COEFFS, TERMS_B, TERMS_L, TERMS_PE, TERMS_R, TERMS_Y,
22};
23
24const SUNRISE_SUNSET_ANGLE: f64 = -0.83337;
26
27const ABERRATION_CONSTANT: f64 = -20.4898;
29
30const EARTH_FLATTENING_FACTOR: f64 = 0.99664719;
32
33const EARTH_RADIUS_METERS: f64 = 6378140.0;
35
36const SECONDS_PER_HOUR: f64 = 3600.0;
38
39pub fn solar_position<Tz: TimeZone>(
76 datetime: DateTime<Tz>,
77 latitude: f64,
78 longitude: f64,
79 elevation: f64,
80 delta_t: f64,
81 pressure: f64,
82 temperature: f64,
83) -> Result<SolarPosition> {
84 check_coordinates(latitude, longitude)?;
85
86 let jd = JulianDate::from_datetime(&datetime, delta_t)?;
88
89 let jme = jd.julian_ephemeris_millennium();
90 let jce = jd.julian_ephemeris_century();
91
92 let l_terms = calculate_lbr_terms(jme, TERMS_L);
94 let l_degrees =
95 normalize_degrees_0_to_360(radians_to_degrees(calculate_lbr_polynomial(jme, &l_terms)));
96
97 let b_terms = calculate_lbr_terms(jme, TERMS_B);
99 let b_degrees =
100 normalize_degrees_0_to_360(radians_to_degrees(calculate_lbr_polynomial(jme, &b_terms)));
101
102 let r_terms = calculate_lbr_terms(jme, TERMS_R);
104 let r = calculate_lbr_polynomial(jme, &r_terms);
105
106 if r == 0.0 {
107 return Err(Error::computation_error("Earth radius vector is zero"));
108 }
109
110 let theta_degrees = normalize_degrees_0_to_360(l_degrees + 180.0);
112 let beta_degrees = -b_degrees;
113 let beta = degrees_to_radians(beta_degrees);
114
115 let x_terms = calculate_nutation_terms(jce);
117 let delta_psi_epsilon = calculate_delta_psi_epsilon(jce, &x_terms);
118
119 let epsilon_degrees =
121 calculate_true_obliquity_of_ecliptic(&jd, delta_psi_epsilon.delta_epsilon);
122 let epsilon = degrees_to_radians(epsilon_degrees);
123
124 let delta_tau = ABERRATION_CONSTANT / (SECONDS_PER_HOUR * r);
126
127 let lambda_degrees = theta_degrees + delta_psi_epsilon.delta_psi + delta_tau;
129 let lambda = degrees_to_radians(lambda_degrees);
130
131 let nu_degrees = calculate_apparent_sidereal_time_at_greenwich(
133 &jd,
134 delta_psi_epsilon.delta_psi,
135 epsilon_degrees,
136 );
137
138 let alpha_degrees = calculate_geocentric_sun_right_ascension(beta, epsilon, lambda);
140
141 let delta_degrees =
143 radians_to_degrees(calculate_geocentric_sun_declination(beta, epsilon, lambda));
144
145 let h_degrees = normalize_degrees_0_to_360(nu_degrees + longitude - alpha_degrees);
147 let h = degrees_to_radians(h_degrees);
148
149 let xi_degrees = 8.794 / (3600.0 * r);
151 let xi = degrees_to_radians(xi_degrees);
152 let phi = degrees_to_radians(latitude);
153 let delta = degrees_to_radians(delta_degrees);
154
155 let u = atan(EARTH_FLATTENING_FACTOR * tan(phi));
156 let x = cos(u) + elevation * cos(phi) / EARTH_RADIUS_METERS;
157 let y = EARTH_FLATTENING_FACTOR.mul_add(sin(u), (elevation * sin(phi)) / EARTH_RADIUS_METERS);
158
159 let x1 = (x * sin(xi)).mul_add(-cos(h), cos(delta));
160 let delta_alpha_degrees = radians_to_degrees(atan2(-x * sin(xi) * sin(h), x1));
161 let delta_prime = atan2(
162 y.mul_add(-sin(xi), sin(delta)) * cos(degrees_to_radians(delta_alpha_degrees)),
163 x1,
164 );
165
166 let h_prime_degrees = h_degrees - delta_alpha_degrees;
168 let h_prime = degrees_to_radians(h_prime_degrees);
169
170 calculate_topocentric_solar_position(pressure, temperature, phi, delta_prime, h_prime)
172}
173
174pub fn solar_position_no_refraction<Tz: TimeZone>(
181 datetime: DateTime<Tz>,
182 latitude: f64,
183 longitude: f64,
184 elevation: f64,
185 delta_t: f64,
186) -> Result<SolarPosition> {
187 solar_position(
188 datetime,
189 latitude,
190 longitude,
191 elevation,
192 delta_t,
193 f64::NAN,
194 f64::NAN,
195 )
196}
197
198#[derive(Debug, Clone, Copy)]
200struct DeltaPsiEpsilon {
201 delta_psi: f64,
202 delta_epsilon: f64,
203}
204
205fn calculate_lbr_terms(jme: f64, term_coeffs: &[&[&[f64; 3]]]) -> Vec<f64> {
207 let mut lbr_terms = vec![0.0; term_coeffs.len()];
208
209 for (i, term_set) in term_coeffs.iter().enumerate() {
210 let mut lbr_sum = 0.0;
211 for term in *term_set {
212 lbr_sum += term[0] * cos(term[2].mul_add(jme, term[1]));
213 }
214 lbr_terms[i] = lbr_sum;
215 }
216
217 lbr_terms
218}
219
220fn calculate_lbr_polynomial(jme: f64, terms: &[f64]) -> f64 {
222 polynomial(terms, jme) / 1e8
223}
224
225fn calculate_nutation_terms(jce: f64) -> Vec<f64> {
227 NUTATION_COEFFS
228 .iter()
229 .map(|coeffs| polynomial(coeffs, jce))
230 .collect()
231}
232
233fn calculate_delta_psi_epsilon(jce: f64, x: &[f64]) -> DeltaPsiEpsilon {
235 let mut delta_psi = 0.0;
236 let mut delta_epsilon = 0.0;
237
238 for (i, pe_term) in TERMS_PE.iter().enumerate() {
239 let xj_yterm_sum = degrees_to_radians(calculate_xj_yterm_sum(i, x));
240
241 let delta_psi_contrib = pe_term[1].mul_add(jce, pe_term[0]) * sin(xj_yterm_sum);
243 let delta_epsilon_contrib = pe_term[3].mul_add(jce, pe_term[2]) * cos(xj_yterm_sum);
244
245 delta_psi += delta_psi_contrib;
246 delta_epsilon += delta_epsilon_contrib;
247 }
248
249 DeltaPsiEpsilon {
250 delta_psi: delta_psi / 36_000_000.0,
251 delta_epsilon: delta_epsilon / 36_000_000.0,
252 }
253}
254
255fn calculate_xj_yterm_sum(i: usize, x: &[f64]) -> f64 {
257 let mut sum = 0.0;
258 for (j, &x_val) in x.iter().enumerate() {
259 sum += x_val * f64::from(TERMS_Y[i][j]);
260 }
261 sum
262}
263
264fn calculate_true_obliquity_of_ecliptic(jd: &JulianDate, delta_epsilon: f64) -> f64 {
266 let epsilon0 = polynomial(OBLIQUITY_COEFFS, jd.julian_ephemeris_millennium() / 10.0);
267 epsilon0 / 3600.0 + delta_epsilon
268}
269
270fn calculate_apparent_sidereal_time_at_greenwich(
272 jd: &JulianDate,
273 delta_psi: f64,
274 epsilon_degrees: f64,
275) -> f64 {
276 let nu0_degrees = normalize_degrees_0_to_360(jd.julian_century().powi(2).mul_add(
277 0.000387933 - jd.julian_century() / 38710000.0,
278 360.98564736629f64.mul_add(jd.julian_date() - 2451545.0, 280.46061837),
279 ));
280
281 delta_psi.mul_add(cos(degrees_to_radians(epsilon_degrees)), nu0_degrees)
282}
283
284fn calculate_geocentric_sun_right_ascension(
286 beta_rad: f64,
287 epsilon_rad: f64,
288 lambda_rad: f64,
289) -> f64 {
290 let alpha = atan2(
291 sin(lambda_rad).mul_add(cos(epsilon_rad), -(tan(beta_rad) * sin(epsilon_rad))),
292 cos(lambda_rad),
293 );
294 normalize_degrees_0_to_360(radians_to_degrees(alpha))
295}
296
297fn calculate_geocentric_sun_declination(beta_rad: f64, epsilon_rad: f64, lambda_rad: f64) -> f64 {
299 asin(sin(beta_rad).mul_add(
300 cos(epsilon_rad),
301 cos(beta_rad) * sin(epsilon_rad) * sin(lambda_rad),
302 ))
303}
304
305fn calculate_topocentric_solar_position(
307 pressure: f64,
308 temperature: f64,
309 phi: f64,
310 delta_prime: f64,
311 h_prime: f64,
312) -> Result<SolarPosition> {
313 let sin_phi = sin(phi);
315 let cos_phi = cos(phi);
316 let cos_h_prime = cos(h_prime);
317
318 let e_zero = asin(sin_phi.mul_add(sin(delta_prime), cos_phi * cos(delta_prime) * cos_h_prime));
319 let topocentric_zenith_angle =
320 calculate_topocentric_zenith_angle(pressure, temperature, e_zero);
321
322 let gamma = atan2(
324 sin(h_prime),
325 cos_h_prime.mul_add(sin_phi, -(tan(delta_prime) * cos_phi)),
326 );
327 let gamma_degrees = normalize_degrees_0_to_360(radians_to_degrees(gamma));
328 let topocentric_azimuth_angle = normalize_degrees_0_to_360(gamma_degrees + 180.0);
329
330 SolarPosition::new(topocentric_azimuth_angle, topocentric_zenith_angle)
331}
332
333fn calculate_topocentric_zenith_angle(pressure: f64, temperature: f64, e_zero: f64) -> f64 {
335 let e_zero_degrees = radians_to_degrees(e_zero);
336
337 let do_correct = check_refraction_params_usable(pressure, temperature)
339 && e_zero_degrees > SUNRISE_SUNSET_ANGLE;
340
341 if do_correct {
342 90.0 - e_zero_degrees
343 - (pressure / 1010.0) * (283.0 / (273.0 + temperature)) * 1.02
344 / (60.0
345 * tan(degrees_to_radians(
346 e_zero_degrees + 10.3 / (e_zero_degrees + 5.11),
347 )))
348 } else {
349 90.0 - e_zero_degrees
350 }
351}
352
353pub fn sunrise_sunset<Tz: TimeZone>(
391 date: DateTime<Tz>,
392 latitude: f64,
393 longitude: f64,
394 delta_t: f64,
395 elevation_angle: f64,
396) -> Result<crate::SunriseResult<DateTime<Tz>>> {
397 check_coordinates(latitude, longitude)?;
398
399 let day_start = date
401 .timezone()
402 .from_local_datetime(&date.date_naive().and_hms_opt(0, 0, 0).unwrap())
403 .unwrap();
404
405 calculate_sunrise_sunset_spa_algorithm(day_start, latitude, longitude, delta_t, elevation_angle)
407}
408
409fn calculate_sunrise_sunset_spa_algorithm<Tz: TimeZone>(
411 day: DateTime<Tz>,
412 latitude: f64,
413 longitude: f64,
414 delta_t: f64,
415 elevation_angle: f64,
416) -> Result<crate::SunriseResult<DateTime<Tz>>> {
417 let day_start = day
418 .timezone()
419 .from_local_datetime(&day.date_naive().and_hms_opt(0, 0, 0).unwrap())
420 .unwrap();
421
422 let (nu_degrees, delta_psi_epsilon, epsilon_degrees) =
424 calculate_sidereal_time_and_nutation(day_start.clone());
425
426 let alpha_deltas =
428 calculate_alpha_deltas_for_three_days(day_start, delta_psi_epsilon, epsilon_degrees)?;
429
430 let m0 = (alpha_deltas[1].alpha - longitude - nu_degrees) / 360.0;
432
433 let polar_type = check_polar_conditions_type(latitude, elevation_angle, alpha_deltas[1].delta);
435
436 let (m_values, _h0_degrees) =
438 calculate_approximate_times(m0, latitude, elevation_angle, alpha_deltas[1].delta);
439
440 let final_result = calculate_final_times(FinalTimeParams {
442 day: day.clone(),
443 m_values,
444 nu_degrees,
445 delta_t,
446 latitude,
447 longitude,
448 elevation_angle,
449 alpha_deltas,
450 });
451
452 match polar_type {
454 Some(PolarType::AllDay) => {
455 if let crate::SunriseResult::RegularDay { transit, .. } = final_result {
456 Ok(crate::SunriseResult::AllDay { transit })
457 } else {
458 Ok(final_result)
459 }
460 }
461 Some(PolarType::AllNight) => {
462 if let crate::SunriseResult::RegularDay { transit, .. } = final_result {
463 Ok(crate::SunriseResult::AllNight { transit })
464 } else {
465 Ok(final_result)
466 }
467 }
468 None => Ok(final_result),
469 }
470}
471
472fn calculate_sidereal_time_and_nutation<Tz: TimeZone>(
474 day_start: DateTime<Tz>,
475) -> (f64, DeltaPsiEpsilon, f64) {
476 let jd = JulianDate::from_datetime(&day_start, 0.0).unwrap();
477 let jce_day = jd.julian_ephemeris_century();
478 let x_terms = calculate_nutation_terms(jce_day);
479 let delta_psi_epsilon = calculate_delta_psi_epsilon(jce_day, &x_terms);
480 let epsilon_degrees =
481 calculate_true_obliquity_of_ecliptic(&jd, delta_psi_epsilon.delta_epsilon);
482 let nu_degrees = calculate_apparent_sidereal_time_at_greenwich(
483 &jd,
484 delta_psi_epsilon.delta_psi,
485 epsilon_degrees,
486 );
487 (nu_degrees, delta_psi_epsilon, epsilon_degrees)
488}
489
490fn calculate_alpha_deltas_for_three_days<Tz: TimeZone>(
492 day_start: DateTime<Tz>,
493 delta_psi_epsilon: DeltaPsiEpsilon,
494 epsilon_degrees: f64,
495) -> Result<[AlphaDelta; 3]> {
496 let mut alpha_deltas = [AlphaDelta {
497 alpha: 0.0,
498 delta: 0.0,
499 }; 3];
500 for (i, alpha_delta) in alpha_deltas.iter_mut().enumerate() {
501 let current_jd = JulianDate::from_datetime(&day_start, 0.0)?.add_days((i as f64) - 1.0);
502 let current_jme = current_jd.julian_ephemeris_millennium();
503 let ad = calculate_alpha_delta(current_jme, delta_psi_epsilon.delta_psi, epsilon_degrees);
504 *alpha_delta = ad;
505 }
506 Ok(alpha_deltas)
507}
508
509fn check_polar_conditions<Tz: TimeZone>(
511 day: DateTime<Tz>,
512 m0: f64,
513 latitude: f64,
514 elevation_angle: f64,
515 delta1: f64,
516) -> Option<crate::SunriseResult<DateTime<Tz>>> {
517 let phi = degrees_to_radians(latitude);
518 let delta1_rad = degrees_to_radians(delta1);
519 let elevation_rad = degrees_to_radians(elevation_angle);
520
521 let acos_arg =
522 sin(phi).mul_add(-sin(delta1_rad), sin(elevation_rad)) / (cos(phi) * cos(delta1_rad));
523
524 if acos_arg < -1.0 {
525 let transit = add_fraction_of_day(day, m0);
526 Some(crate::SunriseResult::AllDay { transit })
527 } else if acos_arg > 1.0 {
528 let transit = add_fraction_of_day(day, m0);
529 Some(crate::SunriseResult::AllNight { transit })
530 } else {
531 None
532 }
533}
534
535#[derive(Debug, Clone, Copy)]
537enum PolarType {
538 AllDay,
539 AllNight,
540}
541
542fn check_polar_conditions_type(
544 latitude: f64,
545 elevation_angle: f64,
546 delta1: f64,
547) -> Option<PolarType> {
548 let phi = degrees_to_radians(latitude);
549 let elevation_rad = degrees_to_radians(elevation_angle);
550 let delta1_rad = degrees_to_radians(delta1);
551
552 let acos_arg =
553 sin(phi).mul_add(-sin(delta1_rad), sin(elevation_rad)) / (cos(phi) * cos(delta1_rad));
554
555 if acos_arg < -1.0 {
556 Some(PolarType::AllDay)
557 } else if acos_arg > 1.0 {
558 Some(PolarType::AllNight)
559 } else {
560 None
561 }
562}
563
564fn calculate_approximate_times(
566 m0: f64,
567 latitude: f64,
568 elevation_angle: f64,
569 delta1: f64,
570) -> ([f64; 3], f64) {
571 let phi = degrees_to_radians(latitude);
572 let delta1_rad = degrees_to_radians(delta1);
573 let elevation_rad = degrees_to_radians(elevation_angle);
574
575 let acos_arg =
576 sin(phi).mul_add(-sin(delta1_rad), sin(elevation_rad)) / (cos(phi) * cos(delta1_rad));
577 let h0 = acos(acos_arg);
578 let h0_degrees = radians_to_degrees(h0).min(180.0);
579
580 let mut m = [0.0; 3];
581 m[0] = normalize_to_unit_range(m0);
582 m[1] = normalize_to_unit_range(m0 - h0_degrees / 360.0);
583 m[2] = normalize_to_unit_range(m0 + h0_degrees / 360.0);
584
585 (m, h0_degrees)
586}
587
588fn calculate_final_times<Tz: TimeZone>(
590 params: FinalTimeParams<Tz>,
591) -> crate::SunriseResult<DateTime<Tz>> {
592 let mut nu = [0.0; 3];
594 for (i, nu_item) in nu.iter_mut().enumerate() {
595 *nu_item = 360.985647f64.mul_add(params.m_values[i], params.nu_degrees);
596 }
597
598 let mut n = [0.0; 3];
600 for (i, n_item) in n.iter_mut().enumerate() {
601 *n_item = params.m_values[i] + params.delta_t / 86400.0;
602 }
603
604 let alpha_delta_primes = calculate_interpolated_alpha_deltas(¶ms.alpha_deltas, &n);
606
607 let mut h_prime = [0.0; 3];
609 for i in 0..3 {
610 let h_prime_i = nu[i] + params.longitude - alpha_delta_primes[i].alpha;
611 h_prime[i] = limit_h_prime(h_prime_i);
612 }
613
614 let phi = degrees_to_radians(params.latitude);
616 let mut h = [0.0; 3];
617 for i in 0..3 {
618 let delta_prime_rad = degrees_to_radians(alpha_delta_primes[i].delta);
619 h[i] = radians_to_degrees(asin(sin(phi).mul_add(
620 sin(delta_prime_rad),
621 cos(phi) * cos(delta_prime_rad) * cos(degrees_to_radians(h_prime[i])),
622 )));
623 }
624
625 let t = params.m_values[0] - h_prime[0] / 360.0;
627 let r = params.m_values[1]
628 + (h[1] - params.elevation_angle)
629 / (360.0
630 * cos(degrees_to_radians(alpha_delta_primes[1].delta))
631 * cos(phi)
632 * sin(degrees_to_radians(h_prime[1])));
633 let s = params.m_values[2]
634 + (h[2] - params.elevation_angle)
635 / (360.0
636 * cos(degrees_to_radians(alpha_delta_primes[2].delta))
637 * cos(phi)
638 * sin(degrees_to_radians(h_prime[2])));
639
640 let sunrise = add_fraction_of_day(params.day.clone(), r);
641 let transit = add_fraction_of_day(params.day.clone(), t);
642 let sunset = add_fraction_of_day(params.day, s);
643
644 crate::SunriseResult::RegularDay {
645 sunrise,
646 transit,
647 sunset,
648 }
649}
650
651fn calculate_interpolated_alpha_deltas(
653 alpha_deltas: &[AlphaDelta; 3],
654 n: &[f64; 3],
655) -> [AlphaDelta; 3] {
656 let a = limit_if_necessary(alpha_deltas[1].alpha - alpha_deltas[0].alpha);
657 let a_prime = limit_if_necessary(alpha_deltas[1].delta - alpha_deltas[0].delta);
658
659 let b = limit_if_necessary(alpha_deltas[2].alpha - alpha_deltas[1].alpha);
660 let b_prime = limit_if_necessary(alpha_deltas[2].delta - alpha_deltas[1].delta);
661
662 let c = b - a;
663 let c_prime = b_prime - a_prime;
664
665 let mut alpha_delta_primes = [AlphaDelta {
666 alpha: 0.0,
667 delta: 0.0,
668 }; 3];
669 for i in 0..3 {
670 alpha_delta_primes[i].alpha =
671 alpha_deltas[1].alpha + (n[i] * (c.mul_add(n[i], a + b))) / 2.0;
672 alpha_delta_primes[i].delta =
673 alpha_deltas[1].delta + (n[i] * (c_prime.mul_add(n[i], a_prime + b_prime))) / 2.0;
674 }
675 alpha_delta_primes
676}
677
678#[derive(Debug, Clone, Copy)]
679struct AlphaDelta {
680 alpha: f64,
681 delta: f64,
682}
683
684#[derive(Debug, Clone)]
686struct FinalTimeParams<Tz: TimeZone> {
687 day: DateTime<Tz>,
688 m_values: [f64; 3],
689 nu_degrees: f64,
690 delta_t: f64,
691 latitude: f64,
692 longitude: f64,
693 elevation_angle: f64,
694 alpha_deltas: [AlphaDelta; 3],
695}
696
697pub fn sunrise_sunset_for_horizon<Tz: TimeZone>(
732 date: DateTime<Tz>,
733 latitude: f64,
734 longitude: f64,
735 delta_t: f64,
736 horizon: Horizon,
737) -> Result<crate::SunriseResult<DateTime<Tz>>> {
738 sunrise_sunset(
739 date,
740 latitude,
741 longitude,
742 delta_t,
743 horizon.elevation_angle(),
744 )
745}
746
747fn calculate_alpha_delta(jme: f64, delta_psi: f64, epsilon_degrees: f64) -> AlphaDelta {
749 let b_terms = calculate_lbr_terms(jme, TERMS_B);
753 let b_degrees =
754 normalize_degrees_0_to_360(radians_to_degrees(calculate_lbr_polynomial(jme, &b_terms)));
755
756 let r_terms = calculate_lbr_terms(jme, TERMS_R);
758 let r = calculate_lbr_polynomial(jme, &r_terms);
759 assert!(r != 0.0);
760
761 let l_terms = calculate_lbr_terms(jme, TERMS_L);
763 let l_degrees =
764 normalize_degrees_0_to_360(radians_to_degrees(calculate_lbr_polynomial(jme, &l_terms)));
765
766 let theta_degrees = normalize_degrees_0_to_360(l_degrees + 180.0);
768
769 let beta_degrees = -b_degrees;
771 let beta = degrees_to_radians(beta_degrees);
772 let epsilon = degrees_to_radians(epsilon_degrees);
773
774 let delta_tau = ABERRATION_CONSTANT / (SECONDS_PER_HOUR * r);
776
777 let lambda_degrees = theta_degrees + delta_psi + delta_tau;
779 let lambda = degrees_to_radians(lambda_degrees);
780
781 let alpha_degrees = calculate_geocentric_sun_right_ascension(beta, epsilon, lambda);
783 let delta_degrees =
784 radians_to_degrees(calculate_geocentric_sun_declination(beta, epsilon, lambda));
785
786 AlphaDelta {
787 alpha: alpha_degrees,
788 delta: delta_degrees,
789 }
790}
791
792fn normalize_to_unit_range(val: f64) -> f64 {
794 let divided = val;
795 let limited = divided - divided.floor();
796 if limited < 0.0 {
797 limited + 1.0
798 } else {
799 limited
800 }
801}
802
803fn add_fraction_of_day<Tz: TimeZone>(day: DateTime<Tz>, fraction: f64) -> DateTime<Tz> {
805 const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000; let millis_plus = (f64::from(MS_PER_DAY) * fraction) as i32; let day_start = day
813 .timezone()
814 .from_local_datetime(&day.date_naive().and_hms_opt(0, 0, 0).unwrap())
815 .unwrap();
816
817 day_start + chrono::Duration::milliseconds(i64::from(millis_plus))
818}
819
820fn limit_if_necessary(val: f64) -> f64 {
822 if val.abs() > 2.0 {
823 normalize_to_unit_range(val)
824 } else {
825 val
826 }
827}
828
829fn limit_h_prime(h_prime: f64) -> f64 {
831 let normalized = h_prime / 360.0;
832 let limited = 360.0 * (normalized - floor(normalized));
833
834 if limited < -180.0 {
835 limited + 360.0
836 } else if limited > 180.0 {
837 limited - 360.0
838 } else {
839 limited
840 }
841}
842
843pub fn sunrise_sunset_multiple<Tz, H>(
889 date: DateTime<Tz>,
890 latitude: f64,
891 longitude: f64,
892 delta_t: f64,
893 horizons: H,
894) -> impl Iterator<Item = Result<(Horizon, crate::SunriseResult<DateTime<Tz>>)>>
895where
896 Tz: TimeZone,
897 H: IntoIterator<Item = Horizon>,
898{
899 let precomputed = (|| -> Result<_> {
901 check_coordinates(latitude, longitude)?;
902
903 let day_start = date
904 .timezone()
905 .from_local_datetime(&date.date_naive().and_hms_opt(0, 0, 0).unwrap())
906 .unwrap();
907 let (nu_degrees, delta_psi_epsilon, epsilon_degrees) =
908 calculate_sidereal_time_and_nutation(day_start.clone());
909 let alpha_deltas =
910 calculate_alpha_deltas_for_three_days(day_start, delta_psi_epsilon, epsilon_degrees)?;
911
912 Ok((nu_degrees, alpha_deltas))
913 })();
914
915 horizons.into_iter().map(move |horizon| match &precomputed {
916 Ok((nu_degrees, alpha_deltas)) => {
917 let result = calculate_sunrise_sunset_spa_algorithm_with_precomputed(
918 date.clone(),
919 latitude,
920 longitude,
921 delta_t,
922 horizon.elevation_angle(),
923 *nu_degrees,
924 *alpha_deltas,
925 );
926 Ok((horizon, result))
927 }
928 Err(e) => Err(e.clone()),
929 })
930}
931
932fn calculate_sunrise_sunset_spa_algorithm_with_precomputed<Tz: TimeZone>(
934 day: DateTime<Tz>,
935 latitude: f64,
936 longitude: f64,
937 delta_t: f64,
938 elevation_angle: f64,
939 nu_degrees: f64,
940 alpha_deltas: [AlphaDelta; 3],
941) -> crate::SunriseResult<DateTime<Tz>> {
942 let day_start = day
944 .timezone()
945 .from_local_datetime(&day.date_naive().and_hms_opt(0, 0, 0).unwrap())
946 .unwrap();
947
948 let m0 = (alpha_deltas[1].alpha - longitude - nu_degrees) / 360.0;
950 if let Some(polar_result) = check_polar_conditions(
951 day_start.clone(),
952 m0,
953 latitude,
954 elevation_angle,
955 alpha_deltas[1].delta,
956 ) {
957 return polar_result;
958 }
959
960 let (m_values, _h0_degrees) =
962 calculate_approximate_times(m0, latitude, elevation_angle, alpha_deltas[1].delta);
963
964 calculate_final_times(FinalTimeParams {
966 day: day_start,
967 m_values,
968 nu_degrees,
969 delta_t,
970 latitude,
971 longitude,
972 elevation_angle,
973 alpha_deltas,
974 })
975}
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980 use chrono::{DateTime, FixedOffset};
981
982 #[test]
983 fn test_spa_basic_functionality() {
984 let datetime = "2023-06-21T12:00:00Z"
985 .parse::<DateTime<FixedOffset>>()
986 .unwrap();
987
988 let result = solar_position(
989 datetime, 37.7749, -122.4194, 0.0, 69.0, 1013.25, 15.0,
991 );
992
993 assert!(result.is_ok());
994 let position = result.unwrap();
995 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
996 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
997 }
998
999 #[test]
1000 fn test_sunrise_sunset_multiple() {
1001 let datetime = "2023-06-21T12:00:00Z"
1002 .parse::<DateTime<FixedOffset>>()
1003 .unwrap();
1004 let horizons = [
1005 Horizon::SunriseSunset,
1006 Horizon::CivilTwilight,
1007 Horizon::NauticalTwilight,
1008 ];
1009
1010 let results: std::result::Result<Vec<_>, _> = sunrise_sunset_multiple(
1011 datetime,
1012 37.7749, -122.4194, 69.0, horizons.iter().copied(),
1016 )
1017 .collect();
1018
1019 let results = results.unwrap();
1020
1021 assert_eq!(results.len(), 3);
1023
1024 let returned_horizons: std::collections::HashSet<_> =
1026 results.iter().map(|(h, _)| *h).collect();
1027 for expected_horizon in horizons {
1028 assert!(returned_horizons.contains(&expected_horizon));
1029 }
1030
1031 for (horizon, bulk_result) in &results {
1033 let individual_result =
1034 sunrise_sunset_for_horizon(datetime, 37.7749, -122.4194, 69.0, *horizon).unwrap();
1035
1036 match (&individual_result, bulk_result) {
1038 (
1039 crate::SunriseResult::RegularDay {
1040 sunrise: s1,
1041 transit: t1,
1042 sunset: ss1,
1043 },
1044 crate::SunriseResult::RegularDay {
1045 sunrise: s2,
1046 transit: t2,
1047 sunset: ss2,
1048 },
1049 ) => {
1050 assert_eq!(s1, s2);
1051 assert_eq!(t1, t2);
1052 assert_eq!(ss1, ss2);
1053 }
1054 (
1055 crate::SunriseResult::AllDay { transit: t1 },
1056 crate::SunriseResult::AllDay { transit: t2 },
1057 )
1058 | (
1059 crate::SunriseResult::AllNight { transit: t1 },
1060 crate::SunriseResult::AllNight { transit: t2 },
1061 ) => {
1062 assert_eq!(t1, t2);
1063 }
1064 _ => panic!("Bulk and individual results differ in type for {horizon:?}"),
1065 }
1066 }
1067 }
1068
1069 #[test]
1070 fn test_spa_no_refraction() {
1071 let datetime = "2023-06-21T12:00:00Z"
1072 .parse::<DateTime<FixedOffset>>()
1073 .unwrap();
1074
1075 let result = solar_position_no_refraction(datetime, 37.7749, -122.4194, 0.0, 69.0);
1076
1077 assert!(result.is_ok());
1078 let position = result.unwrap();
1079 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
1080 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
1081 }
1082
1083 #[test]
1084 fn test_spa_coordinate_validation() {
1085 let datetime = "2023-06-21T12:00:00Z"
1086 .parse::<DateTime<FixedOffset>>()
1087 .unwrap();
1088
1089 assert!(solar_position(datetime, 95.0, 0.0, 0.0, 0.0, 1013.25, 15.0).is_err());
1091
1092 assert!(solar_position(datetime, 0.0, 185.0, 0.0, 0.0, 1013.25, 15.0).is_err());
1094 }
1095
1096 #[test]
1097 fn test_sunrise_sunset_basic() {
1098 let date = "2023-06-21T00:00:00Z"
1099 .parse::<DateTime<FixedOffset>>()
1100 .unwrap();
1101
1102 let result = sunrise_sunset(date, 37.7749, -122.4194, 69.0, -0.833);
1103 assert!(result.is_ok());
1104
1105 let result =
1106 sunrise_sunset_for_horizon(date, 37.7749, -122.4194, 69.0, Horizon::SunriseSunset);
1107 assert!(result.is_ok());
1108 }
1109
1110 #[test]
1111 fn test_horizon_enum() {
1112 assert_eq!(Horizon::SunriseSunset.elevation_angle(), -0.83337);
1113 assert_eq!(Horizon::CivilTwilight.elevation_angle(), -6.0);
1114 assert_eq!(Horizon::Custom(-10.5).elevation_angle(), -10.5);
1115 }
1116}