use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Purpose {
#[default]
General,
Contract,
Partnership,
Travel,
Health,
Finance,
Launch,
Career,
}
impl Purpose {
pub(super) fn house(self) -> Option<usize> {
match self {
Purpose::General => None,
Purpose::Contract | Purpose::Partnership => Some(7),
Purpose::Travel => Some(9),
Purpose::Health => Some(1),
Purpose::Finance => Some(2),
Purpose::Launch | Purpose::Career => Some(10),
}
}
pub(super) fn significator(self) -> Option<&'static str> {
match self {
Purpose::General => None,
Purpose::Contract | Purpose::Travel => Some("Mercury"),
Purpose::Partnership => Some("Venus"),
Purpose::Health | Purpose::Career => Some("Sun"),
Purpose::Finance | Purpose::Launch => Some("Jupiter"),
}
}
pub(super) fn hour_rulers(self) -> &'static [&'static str] {
match self {
Purpose::General => &["Jupiter", "Venus"],
Purpose::Contract => &["Mercury", "Jupiter"],
Purpose::Partnership => &["Venus", "Moon"],
Purpose::Travel => &["Mercury", "Moon"],
Purpose::Health => &["Sun", "Jupiter"],
Purpose::Finance => &["Jupiter", "Venus"],
Purpose::Launch => &["Jupiter", "Sun"],
Purpose::Career => &["Sun", "Jupiter"],
}
}
pub(super) fn fears_mercury_retrograde(self) -> bool {
matches!(self, Purpose::Contract | Purpose::Travel | Purpose::Launch)
}
pub(super) fn fears_venus_retrograde(self) -> bool {
matches!(self, Purpose::Partnership)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HourRange {
pub from: u8,
pub to: u8,
}
impl HourRange {
pub(super) fn contains(&self, minute_of_day: i64) -> bool {
let from = i64::from(self.from) * 60;
let to = i64::from(self.to) * 60;
if from <= to {
(from..to).contains(&minute_of_day)
} else {
minute_of_day >= from || minute_of_day < to
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UtcOffset {
pub from: String,
pub minutes: i32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NatalPoint {
pub name: String,
pub ecliptic_longitude: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ElectionCriteria {
pub purpose: Purpose,
pub avoid_void_moon: bool,
pub avoid_mercury_retrograde: Option<bool>,
pub avoid_venus_retrograde: Option<bool>,
pub daytime_only: bool,
pub local_hours: Option<HourRange>,
pub utc_offsets: Vec<UtcOffset>,
pub step_minutes: u32,
pub min_score: f64,
pub max_results: usize,
pub natal: Option<Vec<NatalPoint>>,
}
impl Default for ElectionCriteria {
fn default() -> Self {
ElectionCriteria {
purpose: Purpose::General,
avoid_void_moon: true,
avoid_mercury_retrograde: None,
avoid_venus_retrograde: None,
daytime_only: false,
local_hours: None,
utc_offsets: Vec::new(),
step_minutes: 10,
min_score: 60.0,
max_results: 20,
natal: None,
}
}
}
impl ElectionCriteria {
pub fn from_value(value: &Value) -> Result<Self, EngineError> {
if value.is_null() {
return Ok(ElectionCriteria::default());
}
serde_json::from_value(value.clone())
.map_err(|e| EngineError::InvalidInput(format!("election criteria: {e}")))
}
pub(super) fn resolve(&self) -> Result<Resolved, EngineError> {
if let Some(range) = self.local_hours {
if range.from > 23 || range.to == 0 || range.to > 24 || range.from == range.to {
return Err(EngineError::InvalidInput(
"local_hours needs 0 <= from <= 23, 1 <= to <= 24 and from != to".into(),
));
}
}
let mut offsets = self
.utc_offsets
.iter()
.map(|o| {
if o.minutes.abs() > 18 * 60 {
return Err(EngineError::InvalidInput(format!(
"UTC offset {} is out of range",
o.minutes
)));
}
UtcInstant::parse(&o.from)
.map(|at| (at, o.minutes))
.map_err(|e| EngineError::InvalidInput(format!("utc_offsets: {e}")))
})
.collect::<Result<Vec<_>, _>>()?;
offsets.sort_by_key(|(at, _)| *at);
if !self.min_score.is_finite() {
return Err(EngineError::InvalidInput(
"min_score must be a number".into(),
));
}
Ok(Resolved {
summary: CriteriaSummary {
purpose: self.purpose,
avoid_void_moon: self.avoid_void_moon,
avoid_mercury_retrograde: self
.avoid_mercury_retrograde
.unwrap_or(self.purpose.fears_mercury_retrograde()),
avoid_venus_retrograde: self
.avoid_venus_retrograde
.unwrap_or(self.purpose.fears_venus_retrograde()),
daytime_only: self.daytime_only,
local_hours: self.local_hours,
step_minutes: self.step_minutes.clamp(5, 60),
min_score: self.min_score.clamp(0.0, 100.0),
max_results: self.max_results.clamp(1, 50),
natal: self.natal.is_some(),
},
offsets,
natal: self.natal.clone().unwrap_or_default(),
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CriteriaSummary {
pub purpose: Purpose,
pub avoid_void_moon: bool,
pub avoid_mercury_retrograde: bool,
pub avoid_venus_retrograde: bool,
pub daytime_only: bool,
pub local_hours: Option<HourRange>,
pub step_minutes: u32,
pub min_score: f64,
pub max_results: usize,
pub natal: bool,
}
pub(super) struct Resolved {
pub(super) summary: CriteriaSummary,
pub(super) offsets: Vec<(UtcInstant, i32)>,
pub(super) natal: Vec<NatalPoint>,
}
impl Resolved {
pub(super) fn local_minute_of_day(&self, instant: UtcInstant) -> i64 {
let offset = self
.offsets
.iter()
.rev()
.find(|(at, _)| *at <= instant)
.or(self.offsets.first())
.map_or(0, |(_, minutes)| *minutes);
let (_, _, _, hh, mm, ..) = instant.add_micros(i64::from(offset) * 60_000_000).civil();
i64::from(hh) * 60 + i64::from(mm)
}
}