// SunCalc 2.0.1: quadratic two-hour scan followed by Newton refinement.
///|
pub(all) struct MoonTimes {
rise : Instant?
set : Instant?
state : HorizonState
} derive(Eq, Debug)
///|
fn moon_height(date : Instant, site : Observer) -> Double {
let p = moon_position(date, site)
p.altitude + 0.2725 * @math.asin(EarthRadius / p.distance) / Rad + 0.09
}
///|
fn hours_later(date : Instant, hours : Double) -> Instant {
{ ms: @math.trunc(date.ms + hours * DayMs / 24.0), }
}
///|
fn refine_moon_cross(ms : Double, site : Observer) -> Instant {
let mut t = ms
for i = 0; i < 2; i = i + 1 {
let h = moon_height({ ms: @math.trunc(t), }, site)
let dh = (
moon_height({ ms: @math.trunc(t + 30000.0), }, site) -
moon_height({ ms: @math.trunc(t - 30000.0), }, site)
) /
60000.0
// A grazing root may have zero slope. Keep the finite sampler/refinement value
// rather than returning an invalid Instant or taking an unbounded Newton step.
let step = h / dh
if !finite(step) || dh.abs() < 1.0e-15 || step.abs() > 7200000.0 {
break
}
t -= step
}
{ ms: @math.trunc(t), }
}
///|
/// Scan the UTC calendar day containing date, independent of host timezone.
/// One-sided days keep the missing event as None. Does not accept a local civil day.
pub fn moon_times(date : Instant, site : Observer) -> MoonTimes {
let t = date.utc_midnight()
let mut h0 = moon_height(t, site)
let mut rise : Double? = None
let mut set : Double? = None
let mut h_max = h0
for i = 1; i <= 24; i = i + 2 {
let h1 = moon_height(hours_later(t, i.to_double()), site)
let h2 = moon_height(hours_later(t, (i + 1).to_double()), site)
h_max = h_max.max(h1).max(h2)
let (roots, x1, x2, ye) = interval_roots(h0, h1, h2)
if roots == 1 {
if h0 < 0.0 {
rise = Some(i.to_double() + x1)
} else {
set = Some(i.to_double() + x1)
}
} else if roots == 2 {
rise = Some(i.to_double() + (if ye < 0.0 { x2 } else { x1 }))
set = Some(i.to_double() + (if ye < 0.0 { x1 } else { x2 }))
}
if rise is Some(_) && set is Some(_) {
break
}
h0 = h2
}
let state = if rise is Some(_) || set is Some(_) {
Crosses
} else if h_max > 0.0 {
AlwaysAbove
} else {
AlwaysBelow
}
{
rise: rise.map(fn(h) { refine_moon_cross(hours_later(t, h).ms, site) }),
set: set.map(fn(h) { refine_moon_cross(hours_later(t, h).ms, site) }),
state,
}
}