diff --git a/Cargo.toml b/Cargo.toml index 38205a6..faf40e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ serde = "1.0.188" serde_json = "1.0.107" rand = "0.8.5" rstest = "0.18.2" +thiserror = "1.0.56" [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..22f3357 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,17 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum SimulationError { + #[error("Type error")] + TypeError(#[from] TypeError), + #[error("Computed results are invalid")] + InvalidInvestmentResults, +} + +#[derive(Error, Debug)] +pub enum TypeError { + #[error("PositiveFloat cannot store `{0}`")] + PositiveFloatError(f64), + #[error("NaN is invalid")] + NaNInvalid, +} diff --git a/src/investment.rs b/src/investment.rs index e11a798..58c6053 100644 --- a/src/investment.rs +++ b/src/investment.rs @@ -1,5 +1,7 @@ +use crate::error; use crate::types::PositiveFloat; use fake::Dummy; +use serde; #[derive(Debug, Clone, Dummy)] pub struct Investment { @@ -24,7 +26,7 @@ impl Investment { } } - pub fn simulate(&self) -> Vec { + pub fn simulate(&self) -> Result, error::SimulationError> { let mut simulation_results: Vec = Vec::new(); for (i, year) in (0..self.investment_years).enumerate() { @@ -36,8 +38,7 @@ impl Investment { + self.annual_net_contributions[i].0 } } - .try_into() - .expect("Net contribution canot be negative"); + .try_into()?; let initial_balance = { if year == 0 { self.initial_deposit.0 + self.annual_net_contributions[i].0 @@ -51,31 +52,43 @@ impl Investment { net_contribution, initial_balance, self.return_rates[i], - ) - .expect("NaN value found"); + )?; simulation_results.push(investment_snapshot); } - simulation_results + Ok(simulation_results) } +} - pub fn results(&self, investment_status: Vec) -> String { - let last_year_result = investment_status - .last() - .expect("Error getting the last year status for the total results"); - format!( - " - ------------------------------------------------------------- - After {} years, these are the total results of the Investment: - -------------------------------------------------------------- - Net contributions: {} - Final balance: {} - ", - self.investment_years, - last_year_result.net_contribution.0, - last_year_result.final_balance() - ) - } +#[derive(serde::Serialize, Debug)] +pub struct InvestmentResult { + investment_years: usize, + net_contributions: PositiveFloat, + final_balance: f64, + average_return_rate: f64, +} + +pub fn get_investment_result( + investment_information: Vec, +) -> Result { + let last_year_result = match investment_information.last() { + Some(result) => result, + None => return Err(error::SimulationError::InvalidInvestmentResults), + }; + + let sum: f64 = investment_information + .iter() + .map(|snapshot| snapshot.return_rate) + .sum(); + let average_return_rate = sum / investment_information.len() as f64; + let investment_result = InvestmentResult { + investment_years: investment_information.len(), + net_contributions: last_year_result.net_contribution, + final_balance: last_year_result.final_balance, + average_return_rate, + }; + + Ok(investment_result) } #[derive(Debug, Clone, Copy)] @@ -93,9 +106,9 @@ impl InvestmentSnapshot { net_contribution: PositiveFloat, initial_balance: f64, return_rate: f64, - ) -> Result { + ) -> Result { if initial_balance.is_nan() || return_rate.is_nan() { - return Err("NaN value provided"); + return Err(error::TypeError::NaNInvalid); } Ok(InvestmentSnapshot { year, @@ -120,6 +133,7 @@ impl InvestmentSnapshot { } } +#[derive(serde::Serialize)] pub struct InvestmentSnapshotResult { year: usize, net_contribution: PositiveFloat, @@ -128,27 +142,6 @@ pub struct InvestmentSnapshotResult { final_balance: f64, } -impl InvestmentSnapshotResult { - pub fn report(&self) -> String { - format!( - " - ----------------- - | YEAR {} - ----------------- - Net contribution: {} - Initial balance: {} - Return rate: {} - Final balance: {} - ", - self.year, - self.net_contribution.0, - self.initial_balance, - self.return_rate, - self.final_balance - ) - } -} - #[cfg(test)] mod investment_status_tests { use super::InvestmentSnapshot; @@ -236,7 +229,7 @@ mod test_investment { AnnualContribution::Single(PositiveFloat(0.0)).to_annual_contributions(3), Interest::Single(0.05).to_interest_rates(3), ); - let investment_results = investment.simulate(); + let investment_results = investment.simulate().unwrap(); let expected: [f64; 3] = [10500.0, 11025.0, 11576.25]; for (i, result) in investment_results.iter().enumerate() { @@ -252,7 +245,7 @@ mod test_investment { AnnualContribution::Single(PositiveFloat(3600.0)).to_annual_contributions(3), vec![0.05, 0.05, 0.05], ); - let investment_results = investment.simulate(); + let investment_results = investment.simulate().unwrap(); let expected: [f64; 3] = [14280.0, 18774.0, 23492.7]; for (i, result) in investment_results.iter().enumerate() { @@ -268,7 +261,7 @@ mod test_investment { AnnualContribution::Single(PositiveFloat(3600.0)).to_annual_contributions(3), vec![0.05, -0.05, -0.05], ); - let investment_results = investment.simulate(); + let investment_results = investment.simulate().unwrap(); let expected: [f64; 3] = [14280.0, 16986.0, 19556.7]; for (i, result) in investment_results.iter().enumerate() { diff --git a/src/main.rs b/src/main.rs index 33b1718..294be49 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,12 @@ use clap::{arg, command, Parser}; use config::{Config, File, FileFormat}; -use serde::Deserialize; mod distributions; +mod error; mod investment; mod types; -use investment::Investment; +use investment::{Investment, InvestmentSnapshotResult}; use types::{AnnualContribution, Interest, PositiveFloat}; #[derive(Parser)] @@ -16,7 +16,7 @@ struct Args { config_file: String, } -#[derive(Deserialize)] +#[derive(serde::Deserialize)] struct Configuration { deposit: usize, interest_rates: Interest, @@ -41,12 +41,21 @@ fn main() { .to_annual_contributions(config.years), config.interest_rates.to_interest_rates(config.years), ); - let investment_snapshots = investment.simulate(); - - for status in investment_snapshots.iter() { - println!("{}", status.result().report()); + let investment_snapshots = investment.simulate().unwrap(); + let investment_results: Vec = investment_snapshots + .iter() + .map(|snapshot| snapshot.result()) + .collect(); + for (year, result) in investment_results.iter().enumerate() { + println!( + "Investment result year {}\n {}", + year + 1, + serde_json::to_string(result).unwrap() + ); } - - let results = investment.results(investment_snapshots); - println!("{}", results); + let investment_result = investment::get_investment_result(investment_results).unwrap(); + println!( + "Investment result\n {}", + serde_json::to_string(&investment_result).unwrap() + ); } diff --git a/src/types.rs b/src/types.rs index b1cca67..83083f0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,23 +1,17 @@ -use crate::distributions::get_distributions; +use crate::distributions; +use crate::error; use fake::{Dummy, Faker}; use rand::Rng; -use serde::Deserialize; -#[derive(Copy, Clone, Debug, PartialEq, Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct PositiveFloat(pub f64); -#[derive(Debug, PartialEq)] -pub struct PositiveFloatError(String); - impl TryFrom for PositiveFloat { - type Error = PositiveFloatError; + type Error = error::TypeError; - fn try_from(value: f64) -> Result { + fn try_from(value: f64) -> Result { if value < 0.0 { - return Err(PositiveFloatError(format!( - "{:.2} is a negative float.", - value - ))); + return Err(error::TypeError::PositiveFloatError(value)); } Ok(Self(value)) @@ -30,7 +24,7 @@ impl Dummy for PositiveFloat { } } -#[derive(Deserialize)] +#[derive(serde::Deserialize)] #[serde(untagged)] pub enum Interest { Single(f64), @@ -46,7 +40,7 @@ impl Interest { } Interest::Multiple(multiple) => multiple.to_vec(), Interest::Distribution(dist_name) => { - let distribution = get_distributions() + let distribution = distributions::get_distributions() .get(dist_name.as_str()) .expect("The selected distribution doesn't exist") .to_vec(); @@ -64,7 +58,7 @@ impl Interest { } } -#[derive(Deserialize)] +#[derive(serde::Deserialize)] #[serde(untagged)] pub enum AnnualContribution { Single(PositiveFloat),