1#![allow(clippy::unreadable_literal)]
11#![allow(clippy::many_single_char_names)]
12#![allow(clippy::suboptimal_flops)]
13
14use crate::error::check_coordinates;
15use crate::math::{
16 asin, atan2, cos, degrees_to_radians, floor, normalize_degrees_0_to_360, radians_to_degrees,
17 rem_euclid, sin, sqrt, tan, PI,
18};
19use crate::time::validate_utc_components;
20use crate::{RefractionCorrection, Result, SolarPosition};
21#[cfg(feature = "chrono")]
22use chrono::{DateTime, Datelike, TimeZone, Timelike};
23
24#[cfg(feature = "chrono")]
74#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
75#[allow(clippy::needless_pass_by_value)]
76pub fn solar_position<Tz: TimeZone>(
77 datetime: DateTime<Tz>,
78 latitude: f64,
79 longitude: f64,
80 delta_t: f64,
81 refraction: Option<RefractionCorrection>,
82) -> Result<SolarPosition> {
83 let t = calc_t(&datetime)?;
85 solar_position_from_t(t, latitude, longitude, delta_t, refraction)
86}
87
88pub fn solar_position_from_t(
105 t: f64,
106 latitude: f64,
107 longitude: f64,
108 delta_t: f64,
109 refraction: Option<RefractionCorrection>,
110) -> Result<SolarPosition> {
111 check_coordinates(latitude, longitude)?;
112
113 let t_e = t + 1.1574e-5 * delta_t;
114 let omega_at_e = 0.0172019715 * t_e;
115
116 let lambda = -1.388803
118 + 1.720279216e-2 * t_e
119 + 3.3366e-2 * sin(omega_at_e - 0.06172)
120 + 3.53e-4 * sin(2.0 * omega_at_e - 0.1163);
121
122 let epsilon = 4.089567e-1 - 6.19e-9 * t_e;
124
125 let s_lambda = sin(lambda);
126 let c_lambda = cos(lambda);
127 let s_epsilon = sin(epsilon);
128 let c_epsilon = sqrt(1.0 - s_epsilon * s_epsilon);
129
130 let alpha = rem_euclid(atan2(s_lambda * c_epsilon, c_lambda), 2.0 * PI);
132
133 let delta = asin(s_lambda * s_epsilon);
135
136 let h = rem_euclid(
138 1.7528311 + 6.300388099 * t + degrees_to_radians(longitude) - alpha + PI,
139 2.0 * PI,
140 ) - PI;
141
142 let s_phi = sin(degrees_to_radians(latitude));
144 let c_phi = sqrt(1.0 - s_phi * s_phi);
145 let s_delta = sin(delta);
146 let c_delta = sqrt(1.0 - s_delta * s_delta);
147 let s_h = sin(h);
148 let c_h = cos(h);
149
150 let s_epsilon0 = s_phi * s_delta + c_phi * c_delta * c_h;
151 let e_p = asin(s_epsilon0) - 4.26e-5 * sqrt(1.0 - s_epsilon0 * s_epsilon0);
152 let gamma = atan2(s_h, c_h * s_phi - s_delta * c_phi / c_delta);
153
154 let delta_re = refraction.map_or(0.0, |correction| {
156 if e_p > 0.0 {
157 let pressure = correction.pressure();
158 let temperature = correction.temperature();
159 (0.08422 * (pressure / 1000.0))
160 / ((273.0 + temperature) * tan(e_p + 0.003138 / (e_p + 0.08919)))
161 } else {
162 0.0
163 }
164 });
165
166 let z = PI / 2.0 - e_p - delta_re;
167
168 let azimuth = normalize_degrees_0_to_360(radians_to_degrees(gamma + PI));
169 let zenith = radians_to_degrees(z);
170
171 SolarPosition::new(azimuth, zenith)
172}
173
174pub fn calc_t_from_components(
181 year: i32,
182 month: u32,
183 day: u32,
184 hour: u32,
185 minute: u32,
186 second: f64,
187) -> Result<f64> {
188 validate_utc_components(year, month, day, hour, minute, second)?;
189
190 let mut m = month;
191 let mut y = year;
192 let h = f64::from(hour) + f64::from(minute) / 60.0 + second / 3600.0;
193
194 if m <= 2 {
195 m += 12;
196 y -= 1;
197 }
198
199 Ok(
200 floor(365.25 * f64::from(y - 2000)) + floor(30.6001 * f64::from(m + 1))
201 - floor(0.01 * f64::from(y))
202 + f64::from(day)
203 + 0.0416667 * h
204 - 21958.0,
205 )
206}
207
208#[cfg(feature = "chrono")]
210fn calc_t<Tz: TimeZone>(datetime: &DateTime<Tz>) -> Result<f64> {
211 let utc_datetime = datetime.with_timezone(&chrono::Utc);
213 calc_t_from_components(
214 utc_datetime.year(),
215 utc_datetime.month(),
216 utc_datetime.day(),
217 utc_datetime.hour(),
218 utc_datetime.minute(),
219 f64::from(utc_datetime.second()) + f64::from(utc_datetime.nanosecond()) / 1e9,
220 )
221}
222
223#[cfg(all(test, feature = "chrono"))]
224mod tests {
225 use super::*;
226 use chrono::{DateTime, FixedOffset};
227
228 #[test]
229 fn test_grena3_basic_functionality() {
230 let datetime = "2023-06-21T12:00:00-07:00"
231 .parse::<DateTime<FixedOffset>>()
232 .unwrap();
233
234 let result = solar_position(datetime, 37.7749, -122.4194, 69.0, None);
235
236 assert!(result.is_ok());
237 let position = result.unwrap();
238 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
239 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
240 }
241
242 #[test]
243 fn test_grena3_with_refraction() {
244 let datetime = "2023-06-21T12:00:00-07:00"
245 .parse::<DateTime<FixedOffset>>()
246 .unwrap();
247
248 let result = solar_position(
249 datetime,
250 37.7749,
251 -122.4194,
252 69.0,
253 Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
254 );
255
256 assert!(result.is_ok());
257 let position = result.unwrap();
258 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
259 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
260 }
261
262 #[test]
263 fn test_grena3_coordinate_validation() {
264 let datetime = "2023-06-21T12:00:00-07:00"
265 .parse::<DateTime<FixedOffset>>()
266 .unwrap();
267
268 assert!(solar_position(datetime, 95.0, 0.0, 0.0, None).is_err());
270
271 assert!(solar_position(datetime, 0.0, 185.0, 0.0, None).is_err());
273 }
274
275 #[test]
276 fn test_calc_t() {
277 let datetime = "2023-06-21T12:00:00-07:00"
279 .parse::<DateTime<FixedOffset>>()
280 .unwrap();
281 let t = calc_t(&datetime).unwrap();
282
283 assert!(t.is_finite(), "t should be finite");
286
287 let t2 = calc_t(&datetime).unwrap();
289 assert!(
290 (t - t2).abs() < f64::EPSILON,
291 "calc_t should be deterministic"
292 );
293
294 let datetime2 = "2023-06-22T12:00:00Z"
296 .parse::<DateTime<FixedOffset>>()
297 .unwrap();
298 let t3 = calc_t(&datetime2).unwrap();
299 assert!(
300 (t - t3).abs() > 0.5,
301 "Different dates should give different t values"
302 );
303 }
304
305 #[test]
306 fn test_calc_t_pre_2000_flooring() {
307 let t = calc_t_from_components(1999, 12, 31, 0, 0, 0.0).unwrap();
308 assert_eq!(t, -21915.0);
309 }
310
311 #[test]
312 fn test_calc_t_validation() {
313 assert!(calc_t_from_components(2024, 13, 1, 0, 0, 0.0).is_err());
314 assert!(calc_t_from_components(2024, 1, 32, 0, 0, 0.0).is_err());
315 assert!(calc_t_from_components(2024, 2, 30, 0, 0, 0.0).is_err());
316 assert!(calc_t_from_components(1582, 10, 10, 0, 0, 0.0).is_err());
317 assert!(calc_t_from_components(2024, 1, 1, 24, 0, 0.0).is_err());
318 assert!(calc_t_from_components(2024, 1, 1, 0, 60, 0.0).is_err());
319 assert!(calc_t_from_components(2024, 1, 1, 0, 0, 60.0).is_err());
320 }
321}