use std::collections::BTreeMap;
use crate::plan::{Allocation, Node, Plan, Span, Trigger, TriggerForm};
use crate::tax;
#[derive(Debug)]
pub(crate) struct Resolver {
event_year: BTreeMap<String, Option<i16>>,
income_year: BTreeMap<String, Option<i16>>,
}
impl Resolver {
pub(crate) fn new(plan: &Plan) -> Self {
let mut resolver = Self {
event_year: BTreeMap::new(),
income_year: BTreeMap::new(),
};
for event in &plan.events {
let year = resolve_node(plan, Node::Event(&event.id), 0);
resolver.event_year.insert(event.id.clone(), year);
}
for income in &plan.income {
let year = resolve_node(plan, Node::Income(&income.id), 0);
resolver.income_year.insert(income.id.clone(), year);
}
resolver
}
pub(crate) fn trigger_year(&self, plan: &Plan, trigger: &Trigger) -> Option<i16> {
resolve_form(plan, trigger.form().ok()?, |node| match node {
Node::Event(id) => self.event_year.get(id).copied().flatten(),
Node::Income(id) => self.income_year.get(id).copied().flatten(),
})
}
pub(crate) fn is_active(&self, plan: &Plan, year: i16, span: Span<'_>) -> bool {
if let Some(on) = span.on {
return self.trigger_year(plan, on) == Some(year);
}
let bound = |trigger: Option<&Trigger>, default: i16| match trigger {
Some(trigger) => self.trigger_year(plan, trigger),
None => Some(default),
};
let (Some(start), Some(end)) = (
bound(span.start, plan.plan.start_year),
bound(span.end, super::horizon_year(plan)),
) else {
return false;
};
(start..=end).contains(&year)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Timeline {
pub events: BTreeMap<String, Option<i16>>,
pub income: BTreeMap<String, Window>,
pub contributions: BTreeMap<String, Window>,
pub residency: Vec<Option<i16>>,
pub glide_steps: BTreeMap<String, Vec<Option<i16>>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Window {
pub start: Option<i16>,
pub end: Option<i16>,
pub on: Option<i16>,
}
impl Window {
#[must_use]
pub fn first(&self) -> Option<i16> {
self.start.or(self.on)
}
}
impl Timeline {
#[must_use]
pub fn new(plan: &Plan) -> Self {
let resolver = Resolver::new(plan);
let year = |trigger: Option<&Trigger>| resolver.trigger_year(plan, trigger?);
let window = |span: Span<'_>| Window {
start: year(span.start),
end: year(span.end),
on: year(span.on),
};
let glide_steps = plan.accounts.iter().filter_map(|account| {
let Some(Allocation::GlidePath(steps)) = &account.allocation else {
return None;
};
let years = steps.iter().map(|step| year(Some(&step.from)));
Some((account.id.clone(), years.collect()))
});
Self {
events: resolver.event_year.clone(),
income: plan
.income
.iter()
.map(|income| (income.id.clone(), window(income.span())))
.collect(),
contributions: plan
.contributions
.iter()
.map(|contribution| (contribution.id.clone(), window(contribution.span())))
.collect(),
residency: plan
.residency
.iter()
.map(|residency| year(residency.from.as_ref()))
.collect(),
glide_steps: glide_steps.collect(),
}
}
}
const MAX_DEPTH: u32 = 64;
pub(crate) fn trigger_month(plan: &Plan, trigger: &Trigger) -> Option<i32> {
root_month(plan, trigger, 0)
}
fn root_month(plan: &Plan, trigger: &Trigger, depth: u32) -> Option<i32> {
if depth > MAX_DEPTH {
return None;
}
let (node, offset) = match trigger.form().ok()? {
TriggerForm::Date(date) => return Some(tax::month_index(date.year(), date.0.month())),
TriggerForm::Age { owner, years } => {
return Some(tax::attained_month(plan.person(owner)?.birth, years));
}
TriggerForm::Event { id, offset } => (Node::Event(id), offset),
TriggerForm::Income { id, offset } => (Node::Income(id), offset),
};
let root = match node {
Node::Event(id) => {
let event = plan.events.iter().find(|event| event.id == id)?;
root_month(plan, &event.trigger, depth + 1)?
}
Node::Income(id) => match plan.income_source(id)?.span().first() {
Some(trigger) => root_month(plan, trigger, depth + 1)?,
None => tax::month_index(plan.plan.start_year, 1),
},
};
Some(root + offset * tax::MONTHS_PER_YEAR)
}
fn resolve_node(plan: &Plan, node: Node<'_>, depth: u32) -> Option<i16> {
if depth > MAX_DEPTH {
return None;
}
let trigger: &Trigger = match node {
Node::Event(id) => &plan.events.iter().find(|event| event.id == id)?.trigger,
Node::Income(id) => match plan.income_source(id)?.span().first() {
Some(trigger) => trigger,
None => return Some(plan.plan.start_year),
},
};
resolve_form(plan, trigger.form().ok()?, |node| {
resolve_node(plan, node, depth + 1)
})
}
fn resolve_form(
plan: &Plan,
form: TriggerForm<'_>,
year_of: impl Fn(Node<'_>) -> Option<i16>,
) -> Option<i16> {
match form {
TriggerForm::Date(date) => Some(date.year()),
TriggerForm::Age { owner, years } => plan
.person(owner)
.map(|person| person.birth.year() + i16::from(years)),
TriggerForm::Event { id, offset } => Some(year_of(Node::Event(id))? + offset as i16),
TriggerForm::Income { id, offset } => Some(year_of(Node::Income(id))? + offset as i16),
}
}