use serde::{Deserialize, Serialize};
use super::measure::{Judged, Measure, judge};
use crate::market::History;
use crate::params::TaxTables;
use crate::plan::{Dollars, Phase, Plan, PlanError, SCHEMA_VERSION};
use crate::search::{Progress, RunError};
pub const CEILING_STEPS: usize = 16;
pub const MAX_FACTOR: f64 = 8.0;
const SETTLED_WITHIN: f64 = 100.0;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct ScaledExpense {
pub id: String,
pub amount: Dollars,
#[serde(default)]
pub phases: Vec<Dollars>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpendingCeiling {
pub baseline: Judged,
pub factor: f64,
pub is_capped: bool,
pub expenses: Vec<ScaledExpense>,
pub judged: Judged,
}
struct Search<'a> {
plan: &'a Plan,
tables: &'a TaxTables,
history: &'a History,
measure: Measure,
progress: &'a Progress,
judged: usize,
}
impl Search<'_> {
fn judge_at(&mut self, factor: f64) -> Result<Judged, RunError> {
let mut candidate = self.plan.clone();
apply_spending(&mut candidate, &scaled(self.plan, factor));
self.judged += 1;
judge(
&candidate,
self.tables,
self.history,
self.measure,
self.progress,
)
}
fn bracket(&mut self, baseline: &Judged) -> Result<(Met, Option<f64>), RunError> {
if !baseline.is_met {
let nothing = self.judge_at(0.0)?;
if !nothing.is_met {
return Err(refused(match self.measure {
Measure::Planned => "the plan runs short even with no flexible spending",
Measure::Success(_) => {
"the plan lasts in fewer markets than the target even with no flexible \
spending"
}
}));
}
return Ok(((0.0, nothing), Some(1.0)));
}
let mut met = (1.0, baseline.clone());
while met.0 < MAX_FACTOR {
let doubled = met.0 * 2.0;
let judged = self.judge_at(doubled)?;
if !judged.is_met {
return Ok((met, Some(doubled)));
}
met = (doubled, judged);
}
Ok((met, None))
}
fn bisect(
&mut self,
mut met: Met,
mut failed: f64,
flexible: Dollars,
) -> Result<Met, RunError> {
while self.judged < CEILING_STEPS && (failed - met.0) * flexible as f64 >= SETTLED_WITHIN {
let middle = f64::midpoint(met.0, failed);
let judged = self.judge_at(middle)?;
if judged.is_met {
met = (middle, judged);
} else {
failed = middle;
}
}
Ok(met)
}
}
type Met = (f64, Judged);
fn refused(message: &str) -> RunError {
RunError::refused("expenses", message)
}
fn scaled(plan: &Plan, factor: f64) -> Vec<ScaledExpense> {
let flexible = plan.expenses.iter().filter(|expense| expense.is_flexible());
let at = |amount: Dollars| (amount as f64 * factor).floor() as Dollars;
flexible
.map(|expense| ScaledExpense {
id: expense.id.clone(),
amount: at(expense.amount),
phases: expense
.phases
.iter()
.map(|phase| at(phase.amount))
.collect(),
})
.collect()
}
pub fn spending_ceiling(
plan: &Plan,
tables: &TaxTables,
history: &History,
measure: Measure,
progress: &Progress,
) -> Result<SpendingCeiling, RunError> {
measure.check()?;
let flexible = plan.flexible_spending();
if flexible == 0 {
return Err(refused(
"there is no flexible spending to scale: every expense is essential, one-time or zero",
));
}
let mut search = Search {
plan,
tables,
history,
measure,
progress,
judged: 0,
};
let baseline = search.judge_at(1.0)?;
let (met, failed) = search.bracket(&baseline)?;
let (factor, judged) = match failed {
Some(failed) => search.bisect(met, failed, flexible)?,
None => met,
};
Ok(SpendingCeiling {
baseline,
factor,
is_capped: failed.is_none(),
expenses: scaled(plan, factor),
judged,
})
}
pub fn apply_spending(plan: &mut Plan, expenses: &[ScaledExpense]) {
for scaled in expenses {
let stated = plan.expenses.iter_mut().find(|it| it.id == scaled.id);
if let Some(expense) = stated {
expense.amount = scaled.amount;
for (phase, &amount) in expense.phases.iter_mut().zip(&scaled.phases) {
phase.amount = amount;
}
}
}
}
pub fn spending_overlay(
base: &str,
plan: &Plan,
expenses: &[ScaledExpense],
) -> Result<String, PlanError> {
#[derive(Serialize)]
struct Fragment<'a> {
id: &'a str,
amount: Dollars,
#[serde(skip_serializing_if = "Vec::is_empty")]
phases: Vec<Phase>,
}
#[derive(Serialize)]
struct OverlayDocument<'a> {
schema: u32,
base: &'a str,
expenses: Vec<Fragment<'a>>,
}
let fragments = expenses.iter().map(|scaled| {
let stated = plan.expenses.iter().find(|it| it.id == scaled.id);
let phases = stated.into_iter().flat_map(|it| &it.phases).enumerate();
Fragment {
id: &scaled.id,
amount: scaled.amount,
phases: phases
.map(|(at, phase)| Phase {
from: phase.from.clone(),
amount: scaled.phases.get(at).copied().unwrap_or(phase.amount),
})
.collect(),
}
});
Ok(toml::to_string_pretty(&OverlayDocument {
schema: SCHEMA_VERSION,
base,
expenses: fragments.collect(),
})?)
}