Skip to content

Commit

Permalink
Implement serde_json and custom errors
Browse files Browse the repository at this point in the history
  • Loading branch information
hectorLop committed Jan 8, 2024
1 parent 909c2bb commit 90eb07c
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 74 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
17 changes: 17 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -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,
}
91 changes: 42 additions & 49 deletions src/investment.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::error;
use crate::types::PositiveFloat;
use fake::Dummy;
use serde;

Check warning on line 4 in src/investment.rs

View workflow job for this annotation

GitHub Actions / clippy

this import is redundant

warning: this import is redundant --> src/investment.rs:4:1 | 4 | use serde; | ^^^^^^^^^^ help: remove it entirely | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_component_path_imports = note: `#[warn(clippy::single_component_path_imports)]` on by default

#[derive(Debug, Clone, Dummy)]
pub struct Investment {
Expand All @@ -24,7 +26,7 @@ impl Investment {
}
}

pub fn simulate(&self) -> Vec<InvestmentSnapshot> {
pub fn simulate(&self) -> Result<Vec<InvestmentSnapshot>, error::SimulationError> {
let mut simulation_results: Vec<InvestmentSnapshot> = Vec::new();

for (i, year) in (0..self.investment_years).enumerate() {
Expand All @@ -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
Expand All @@ -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<InvestmentSnapshot>) -> 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<InvestmentSnapshotResult>,
) -> Result<InvestmentResult, error::SimulationError> {
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)]
Expand All @@ -93,9 +106,9 @@ impl InvestmentSnapshot {
net_contribution: PositiveFloat,
initial_balance: f64,
return_rate: f64,
) -> Result<Self, &'static str> {
) -> Result<Self, error::TypeError> {
if initial_balance.is_nan() || return_rate.is_nan() {
return Err("NaN value provided");
return Err(error::TypeError::NaNInvalid);
}
Ok(InvestmentSnapshot {
year,
Expand All @@ -120,6 +133,7 @@ impl InvestmentSnapshot {
}
}

#[derive(serde::Serialize)]
pub struct InvestmentSnapshotResult {
year: usize,
net_contribution: PositiveFloat,
Expand All @@ -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;
Expand Down Expand Up @@ -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() {
Expand All @@ -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() {
Expand All @@ -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() {
Expand Down
29 changes: 19 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -16,7 +16,7 @@ struct Args {
config_file: String,
}

#[derive(Deserialize)]
#[derive(serde::Deserialize)]
struct Configuration {
deposit: usize,
interest_rates: Interest,
Expand All @@ -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<InvestmentSnapshotResult> = 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()
);
}
24 changes: 9 additions & 15 deletions src/types.rs
Original file line number Diff line number Diff line change
@@ -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<f64> for PositiveFloat {
type Error = PositiveFloatError;
type Error = error::TypeError;

fn try_from(value: f64) -> Result<Self, PositiveFloatError> {
fn try_from(value: f64) -> Result<Self, error::TypeError> {
if value < 0.0 {
return Err(PositiveFloatError(format!(
"{:.2} is a negative float.",
value
)));
return Err(error::TypeError::PositiveFloatError(value));
}

Ok(Self(value))
Expand All @@ -30,7 +24,7 @@ impl Dummy<Faker> for PositiveFloat {
}
}

#[derive(Deserialize)]
#[derive(serde::Deserialize)]
#[serde(untagged)]
pub enum Interest {
Single(f64),
Expand All @@ -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();
Expand All @@ -64,7 +58,7 @@ impl Interest {
}
}

#[derive(Deserialize)]
#[derive(serde::Deserialize)]
#[serde(untagged)]
pub enum AnnualContribution {
Single(PositiveFloat),
Expand Down

0 comments on commit 90eb07c

Please sign in to comment.