use std::collections::HashMap; use std::fmt; use std::ops::AddAssign; use serde::Deserialize; use serde::Serialize; #[derive(Clone, Copy, Serialize, Default)] pub struct Capital { principal: f64, interest: f64, } impl Capital { fn total(&self) -> f64 { self.principal + self.interest } } // TODO generate other operations using macros impl AddAssign for Capital { fn add_assign(&mut self, other: Self) { self.principal += other.principal; self.interest += other.interest; } } #[derive(Serialize)] pub struct Simulation { st: SimState, updates: SimUpdates, history: Vec, payed_noupdates: Capital, payed_noprepays: Capital, payed: Capital, payed_amortized: f64, } impl Simulation { pub fn new(principal: f64, i1: f64, years: u32) -> Self { let pending_quotas = years * 12; let i12 = i1 / 12.0; let mut st = SimState { period: 0, principal, i12, monthly: 0., pending_quotas, }; st.calculate_monthly(); let topay_total = st.monthly * pending_quotas as f64; Simulation { st, payed_noupdates: Capital { principal, interest: topay_total - principal, }, payed_noprepays: Capital::default(), payed: Capital::default(), payed_amortized: 0., history: vec![], updates: SimUpdates::default(), } } pub fn run(&mut self, updates: SimUpdates) { self.updates = updates; let mut st = self.st.clone(); // Fist simulation: no prepayments while st.pending_quotas > 0 && st.principal > 0. { let quota = st.step(); self.payed_noprepays += quota.payed; self.apply_updates(&mut st, false); } // Second simulation, the good one let mut st = self.st.clone(); while st.pending_quotas > 0 && st.principal > 0. { let quota = st.step(); self.payed += quota.payed; self.history.push(quota); self.apply_updates(&mut st, true); } } fn apply_updates(&mut self, st: &mut SimState, apply_prepays: bool) { for update in self.updates.get(st.period) { match update { SimUpdate::Amortize(mut principal) => { if apply_prepays { if principal > st.principal { principal = st.principal; }; st.principal -= principal; st.calculate_monthly(); self.payed.principal += principal; self.payed_amortized += principal; } } SimUpdate::SetI1(i1) => { st.i12 = i1 / 12.0; st.calculate_monthly(); } } } } pub fn render_table(&self) { let st = &self.st; println!("\n========================================================"); println!( "=== HIPOTECA: {}€, A {} AÑOS, INTERÉS FIJO {:.2}% ===", st.principal, st.pending_quotas / 12, st.i12 * 12. * 100., ); println!("========================================================"); println!("\n\n# SIMULACIÓN CUOTAS\n"); println!("Year | Mon | Quota ( Amrtzd + Intrst) | Pending"); for st in self.history.iter() { print!("{st}"); for update in flatten_amortizations(self.updates.get(st.period)) { print!(" {update}"); } println!(); } println!("\n\n# A PRIORI\n"); println!( "== Total a pagar: {:.2} ({:.2} cap + {:.2} int)", self.payed_noupdates.total(), self.payed_noupdates.principal, self.payed_noupdates.interest ); println!( "== Los intereses suponen un {:.2}% del total", 100. * self.payed_noupdates.interest / self.payed_noupdates.total() ); println!("\n\n# RESULTADO FINAL\n"); println!( "== Total pagado: {:.2} ({:.2} en cuotas + {:.2} int + {:.2} amortizado)", self.payed.total(), self.payed.principal - self.payed_amortized, self.payed.interest, self.payed_amortized ); println!( "== Los intereses suponen un {:.2}% del total", 100. * self.payed.interest / self.payed.total() ); println!(); } } #[derive(Clone, Serialize)] struct SimState { period: u32, principal: f64, i12: f64, monthly: f64, pending_quotas: u32, } impl SimState { fn calculate_monthly(&mut self) { self.monthly = self.principal * self.i12 / (1.0 - (1.0 + self.i12).powi(-(self.pending_quotas as i32))) } fn capital(&self) -> Capital { let interest = self.i12 * self.principal; let principal = self.monthly - interest; Capital { principal, interest, } } fn step(&mut self) -> Quota { let capital = self.capital(); self.period += 1; self.pending_quotas -= 1; self.principal = (1.0 + self.i12) * self.principal - self.monthly; Quota { period: self.period, payed: capital, pending_principal: self.principal, } } } #[derive(Clone, Serialize)] pub struct Quota { period: u32, payed: Capital, pending_principal: f64, } impl fmt::Display for Quota { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{:4} | {:3} | {:7.2} ({:7.2} + {:7.2}) | {:10.2}", if (self.period - 1) % 12 == 0 { format!("Y{}", (self.period / 12) + 1) } else { // return Ok(()); "".to_string() }, self.period, self.payed.total(), self.payed.principal, self.payed.interest, self.pending_principal ) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PeriodicUpdate { period: u32, from: Option, to: Option, update: SimUpdate, } #[derive(Debug, Default, Serialize, Deserialize)] pub struct SimUpdates { periodic: Vec, by_month: HashMap, } impl SimUpdates { fn get(&self, month: u32) -> Vec { let SimUpdates { periodic, by_month, } = self; let mut ret = vec![]; for PeriodicUpdate { period, from, to, update, } in periodic.iter() { let base = from.unwrap_or(0); if month % period == base && base <= month && to.unwrap_or(month + 1) > month { ret.push(*update); } } if let Some(update) = by_month.get(&month) { ret.push(*update); } // println!(" {self:?}.get({month}) -> {ret:?}"); ret } pub fn and(mut self, other: SimUpdates) -> Self { for p in other.periodic.iter() { self.periodic.push(p.clone()); } for (k, v) in other.by_month { self.by_month.insert(k, v); } self } } #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum SimUpdate { Amortize(f64), SetI1(f64), } impl fmt::Display for SimUpdate { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[MORTGAGE UPDATE: ")?; match self { Self::Amortize(principal) => { write!(f, "{principal:.2} amortized to reduce pending quotas]") } Self::SetI1(i1) => { write!(f, "I1 set to {i1:.2}]") } } } } impl SimUpdate { pub fn every(&self, months: u32) -> SimUpdates { let mut updates = SimUpdates::default(); updates.periodic.push(PeriodicUpdate { period: months, from: None, to: None, update: *self, }); updates } pub fn at(&self, month: u32) -> SimUpdates { let mut updates = SimUpdates::default(); updates.by_month.insert(month, *self); updates } } fn flatten_amortizations(updates: Vec) -> Vec { let mut amortized = 0.; let mut result = vec![]; for update in updates { match update { SimUpdate::Amortize(n) => { amortized += n; } SimUpdate::SetI1(_) => { result.push(update); } } } if amortized > 0. { result.push(SimUpdate::Amortize(amortized)); } result }