use super::{moon, moon::LunarEvent, Location};
use chrono::{DateTime, Duration, TimeZone};
type AltSample<T> = (DateTime<T>, f64);
type CrossingBracket<T> = (AltSample<T>, AltSample<T>);
#[derive(Debug, Clone)]
pub struct BatchRiseSetResult<T: TimeZone> {
pub moonrise: Option<DateTime<T>>,
pub moonset: Option<DateTime<T>>,
pub calculations_performed: usize,
}
pub fn batch_search_rise_and_set<T: TimeZone>(
location: &Location,
date: &DateTime<T>,
threshold: f64,
) -> BatchRiseSetResult<T>
where
T::Offset: std::fmt::Display,
{
let tz = date.timezone();
let start_naive = date.date_naive().and_hms_opt(0, 0, 0).unwrap();
let start = match tz.from_local_datetime(&start_naive) {
chrono::LocalResult::Single(dt) => dt,
_ => {
return BatchRiseSetResult {
moonrise: None,
moonset: None,
calculations_performed: 0,
}
}
};
let end = start.clone() + Duration::hours(24);
let step = Duration::minutes(5);
let mut calculations = 0;
let mut prev_time = start.clone();
let mut prev_alt = moon::lunar_position(location, &prev_time).altitude - threshold;
calculations += 1;
let mut rise_bracket: Option<CrossingBracket<T>> = None;
let mut set_bracket: Option<CrossingBracket<T>> = None;
let mut batch_start = start.clone() + step;
while batch_start <= end {
let mut times: Vec<DateTime<T>> = Vec::with_capacity(4);
for i in 0..4 {
let t = batch_start.clone() + (step * i);
if t > end {
break;
}
times.push(t);
}
if times.is_empty() {
break;
}
let mut alts = [0.0; 4];
for (i, t) in times.iter().enumerate() {
alts[i] = moon::lunar_position(location, t).altitude - threshold;
}
calculations += times.len();
for (i, t) in times.iter().enumerate() {
let curr_alt = alts[i];
if prev_alt < 0.0 && curr_alt >= 0.0 {
rise_bracket = Some(((prev_time.clone(), prev_alt), (t.clone(), curr_alt)));
}
if prev_alt >= 0.0 && curr_alt < 0.0 {
set_bracket = Some(((prev_time.clone(), prev_alt), (t.clone(), curr_alt)));
}
prev_time = t.clone();
prev_alt = curr_alt;
}
batch_start += step * 4;
}
let moonrise = rise_bracket
.map(|(low, high)| batch_refine_crossing(location, &low, &high, threshold, true));
let moonset = set_bracket
.map(|(low, high)| batch_refine_crossing(location, &low, &high, threshold, false));
BatchRiseSetResult {
moonrise,
moonset,
calculations_performed: calculations,
}
}
fn batch_refine_crossing<T: TimeZone>(
location: &Location,
low_candidate: &(DateTime<T>, f64),
high_candidate: &(DateTime<T>, f64),
threshold: f64,
seek_rising: bool,
) -> DateTime<T>
where
T::Offset: std::fmt::Display,
{
let mut low = low_candidate.0.clone();
let mut high = high_candidate.0.clone();
while (high.timestamp() - low.timestamp()).abs() > 1 {
let span_secs = high.timestamp() - low.timestamp();
let mid = low.clone() + Duration::seconds(span_secs / 2);
let mid_alt = moon::lunar_position(location, &mid).altitude - threshold;
if seek_rising {
if mid_alt >= 0.0 {
high = mid;
} else {
low = mid;
}
} else if mid_alt <= 0.0 {
high = mid;
} else {
low = mid;
}
}
high
}
#[inline]
pub fn batch_lunar_altitude<T: TimeZone>(location: &Location, times: &[DateTime<T>; 4]) -> [f64; 4]
where
T::Offset: std::fmt::Display,
{
let mut altitudes = [0.0; 4];
for i in 0..4 {
let pos = moon::lunar_position(location, ×[i]);
altitudes[i] = pos.altitude;
}
altitudes
}
pub fn lunar_event_time_optimized<T: TimeZone>(
location: &Location,
date: &DateTime<T>,
event: LunarEvent,
) -> Option<DateTime<T>>
where
T::Offset: std::fmt::Display,
{
let threshold = -0.834;
match event {
LunarEvent::Moonrise | LunarEvent::Moonset => {
let result = batch_search_rise_and_set(location, date, threshold);
match event {
LunarEvent::Moonrise => result.moonrise,
LunarEvent::Moonset => result.moonset,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::Location;
use chrono::{Duration, Utc};
use chrono_tz::Tz;
#[test]
fn test_batch_search_returns_valid_times() {
let location = Location::new_unchecked(40.7128, -74.0060); let date = Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap();
let tz: Tz = "America/New_York".parse().unwrap();
let date_tz = date.with_timezone(&tz);
let result = batch_search_rise_and_set(&location, &date_tz, -0.834);
assert!(result.calculations_performed > 200);
assert!(result.moonrise.is_some() || result.moonset.is_some());
}
#[test]
fn test_batch_altitude_returns_four_values() {
let location = Location::new_unchecked(40.7128, -74.0060);
let date = Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap();
let times = [
date,
date + Duration::hours(6),
date + Duration::hours(12),
date + Duration::hours(18),
];
let altitudes = batch_lunar_altitude(&location, ×);
for alt in &altitudes {
assert!(*alt >= -90.0 && *alt <= 90.0);
}
}
}