use std::collections::HashMap; use std::fmt; use std::ops::AddAssign; use serde::Serialize; #[derive(Clone, Copy, Serialize)] 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<'a> { st: SimState, updates: SimUpdates<'a>, history: Vec, topay: Capital, payed: Capital, payed_amortized: f64, } impl<'a> Simulation<'a> { 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, topay: Capital { principal, interest: topay_total - principal, }, payed: Capital { principal: 0., interest: 0., }, payed_amortized: 0., history: vec![], updates: SimUpdates::default(), } } pub fn run(&mut self, updates: SimUpdates<'a>) { self.updates = updates; 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); } } fn apply_updates(&mut self, st: &mut SimState) { for update in self.updates.get(st.period) { match update { SimUpdate::Amortize(mut principal) => { if principal > st.principal { principal = st.principal; }; st.principal -= principal; st.calculate_monthly(); self.payed.principal += principal; self.payed_amortized += principal; } } } } 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.topay.total(), self.topay.principal, self.topay.interest ); println!( "== Los intereses suponen un {:.2}% del total", 100. * self.topay.interest / self.topay.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, Default, Serialize)] pub struct SimUpdates<'a> { periodically: Vec<(u32, Vec<&'a SimUpdate>)>, by_month: HashMap>, } impl<'a> SimUpdates<'a> { fn get(&self, month: u32) -> Vec<&'a SimUpdate> { let SimUpdates { periodically, by_month, } = self; let mut ret = vec![]; for (m, updates) in periodically.iter() { if month % m == 0 { for update in updates.iter() { ret.push(*update); } } } if let Some(updates) = by_month.get(&month) { for update in updates.iter() { ret.push(*update); } } // println!(" {self:?}.get({month}) -> {ret:?}"); ret } pub fn and(mut self, other: SimUpdates<'a>) -> Self { for p in other.periodically.iter() { self.periodically.push(p.clone()); } for (k, v) in other.by_month { self.by_month.insert(k, v); } self } } #[derive(Clone, Debug, Serialize)] pub enum SimUpdate { Amortize(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]") } } } } impl SimUpdate { pub fn every(&self, months: u32) -> SimUpdates { let mut updates = SimUpdates::default(); updates.periodically.push((months, vec![self])); updates } pub fn at(&self, month: u32) -> SimUpdates { let mut updates = SimUpdates::default(); updates.by_month.insert(month, vec![self]); updates } } fn flatten_amortizations(updates: Vec<&SimUpdate>) -> Vec { let mut amortized = 0.; let mut result = vec![]; for update in updates { match update { SimUpdate::Amortize(n) => { amortized += n; } } } if amortized > 0. { result.push(SimUpdate::Amortize(amortized)); } result }