Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
nao1215 committed Jul 19, 2024
1 parent fa17e8f commit 9e4480e
Show file tree
Hide file tree
Showing 7 changed files with 388 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Cargo.lock

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
/plot.png
17 changes: 17 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "cic"
version = "0.1.0"
edition = "2021"
authors = ["Naohiro CHIKAMATSU <[email protected]>"]
license = "MIT"
description = "cis - compound interest calculations"
repository = "https://github.com/nao1215/cic"
readme = "README.md"
categories = ["math", "finance"]
keywords = ["cli", "graph"]

[dependencies]
clap = "4.5.9"
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"
plotters = "0.3.4"
99 changes: 97 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,97 @@
# cic
cic - compound interest calculator
# cic - compound interest calculator
The cic command calculates compound interest. The results of the calculation are output as either a bar graph (`plot.png`) or in JSON format.

The cis calculates the total final amount of the investment based on the input values for the principal, monthly contribution, annual interest rate, and the number of years of contribution.

## Build
```bash
$ cargo build --release
```

## Install
```bash
$ cargo install --path .
```

## Usage
```bash
$ cic --help
cis - Calculates Compound Interest.
Output the results of compound interest calculations as either a line graph image or JSON.

Usage: cic [OPTIONS]

Options:
-p, --principal <PRINCIPAL> The principal at the time you started investing. Defaults to 0
-c, --contribution <CONTRIBUTION> The monthly contribution amount. Defaults to 1
-r, --rate <RATE> The annual interest rate (in %). Defaults to 5
-y, --years <YEARS> The number of years for contributions. Defaults to 5
-j, --json Output as JSON. Defaults to false
-h, --help Print help
```

## Example
### Output plot.png

```bash
$ cic --principal 1000000 --contribution 100000 --rate 10 --years 10
```

![plot](./doc/image/plot.png)

### Output json

```shell
$ ./target/debug/cic --principal 1000000 --contribution 100000 --rate 10 --years 5 --json
[
{
"year": 1,
"principal": 1000000.0,
"annual_contribution": 1200000.0,
"total_contribution": 1200000.0,
"annual_interest": 100000.0,
"total_interest": 100000.0,
"total_amount": 2300000.0
},
{
"year": 2,
"principal": 1000000.0,
"annual_contribution": 1200000.0,
"total_contribution": 2400000.0,
"annual_interest": 230000.0,
"total_interest": 330000.0,
"total_amount": 3730000.0
},
{
"year": 3,
"principal": 1000000.0,
"annual_contribution": 1200000.0,
"total_contribution": 3600000.0,
"annual_interest": 373000.0,
"total_interest": 703000.0,
"total_amount": 5303000.0
},
{
"year": 4,
"principal": 1000000.0,
"annual_contribution": 1200000.0,
"total_contribution": 4800000.0,
"annual_interest": 530300.0,
"total_interest": 1233300.0,
"total_amount": 7033300.0
},
{
"year": 5,
"principal": 1000000.0,
"annual_contribution": 1200000.0,
"total_contribution": 6000000.0,
"annual_interest": 703330.0,
"total_interest": 1936630.0,
"total_amount": 8936630.0
}
]
```

## License
MIT

Binary file added doc/image/plot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
200 changes: 200 additions & 0 deletions src/calculations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
use plotters::prelude::*;
use serde::Serialize;

/// Represents an investment with principal, contribution, interest rate, and duration.
#[derive(Debug)]
pub struct Investment {
/// The initial amount of money invested.
pub principal: f64,
/// The monthly contribution added to the investment.
pub contribution: f64,
/// The annual interest rate as a percentage.
pub rate: f64,
/// The number of years the money is invested for.
pub years: i32,
}

impl Investment {
/// Creates an `Investment` instance from command line arguments.
///
/// # Arguments
///
/// * `matches` - The command line argument matches containing investment parameters.
///
/// # Returns
///
/// Returns an `Investment` instance with values parsed from command line arguments.
///
/// # Example
///
/// ```
/// let matches = clap::App::new("investment")
/// .arg(clap::Arg::new("principal").default_value("0"))
/// .arg(clap::Arg::new("contribution").default_value("1"))
/// .arg(clap::Arg::new("rate").default_value("5"))
/// .arg(clap::Arg::new("years").default_value("5"))
/// .get_matches();
/// let investment = Investment::from_matches(&matches);
/// ```
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
Self {
principal: matches
.get_one::<String>("principal")
.and_then(|s| s.parse().ok())
.unwrap_or(0.0),
contribution: matches
.get_one::<String>("contribution")
.and_then(|s| s.parse().ok())
.unwrap_or(1.0),
rate: matches
.get_one::<String>("rate")
.and_then(|s| s.parse().ok())
.unwrap_or(5.0),
years: matches
.get_one::<String>("years")
.and_then(|s| s.parse().ok())
.unwrap_or(0),
}
}

/// Generates a yearly summary of the investment.
///
/// # Returns
///
/// Returns a vector of `YearlySummary` structs, each representing the investment's status at the end of each year.
///
/// # Example
///
/// ```
/// let investment = Investment {
/// principal: 1000.0,
/// contribution: 100.0,
/// rate: 5.0,
/// years: 10,
/// };
/// let summary = investment.yearly_summary();
/// ```
pub fn yearly_summary(&self) -> Vec<YearlySummary> {
let rate_per_period = self.rate / 100.0;
let mut amount = self.principal;
let mut total_interest = 0.0;
let mut summary = Vec::with_capacity(self.years as usize);

for year in 1..=self.years {
let annual_contribution = self.contribution * 12.0;
let annual_interest = amount * rate_per_period;
total_interest += annual_interest;

amount += annual_contribution + annual_interest;

summary.push(YearlySummary {
year,
principal: self.principal,
annual_contribution,
total_contribution: self.contribution * 12.0 * year as f64,
annual_interest,
total_interest,
total_amount: amount,
});
}
summary
}
}

/// Represents a summary of the investment at the end of a given year.
#[derive(Debug, Serialize)]
pub struct YearlySummary {
/// The year for which the summary is provided.
pub year: i32,
/// The initial principal amount of the investment.
pub principal: f64,
/// The total contribution made during the year.
pub annual_contribution: f64,
/// The cumulative total contribution up to the end of the year.
pub total_contribution: f64,
/// The interest earned during the year.
pub annual_interest: f64,
/// The cumulative total interest earned up to the end of the year.
pub total_interest: f64,
/// The total amount of money at the end of the year.
pub total_amount: f64,
}

/// Plots the investment summary as a line chart.
///
/// # Arguments
///
/// * `summary` - A slice of `YearlySummary` structs representing the investment's progress over time.
///
/// # Returns
///
/// Returns `Result<(), Box<dyn std::error::Error>>` indicating success or failure of the plotting process.
///
/// # Example
///
/// ```no_run
/// let summary = vec![
/// YearlySummary { year: 1, principal: 1000.0, annual_contribution: 1200.0, total_contribution: 1200.0, annual_interest: 50.0, total_interest: 50.0, total_amount: 2150.0 },
/// // Add more summaries here
/// ];
/// plot_summary(&summary).expect("Failed to plot summary");
/// ```
pub fn plot_summary(summary: &[YearlySummary]) -> Result<(), Box<dyn std::error::Error>> {
let root = BitMapBackend::new("plot.png", (600, 400)).into_drawing_area();
root.fill(&WHITE)?;

let mut chart = ChartBuilder::on(&root)
.caption("Investment Summary", ("sans-serif", 30).into_font())
.x_label_area_size(35)
.y_label_area_size(100)
.margin(20)
.build_cartesian_2d(
1..summary.len(),
0.0..summary
.iter()
.map(|s| s.total_amount)
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap(),
)?;

chart
.configure_mesh()
.x_desc("Year")
.y_desc("Amount")
.draw()?;

let years: Vec<usize> = summary.iter().map(|s| s.year as usize).collect();
let mut principal_and_contribution: Vec<f64> = Vec::new();
let mut accumulated_principal_and_contribution = 0.0;
for s in summary {
accumulated_principal_and_contribution += s.annual_contribution;
principal_and_contribution.push(s.principal + accumulated_principal_and_contribution);
}
let total_amount: Vec<f64> = summary.iter().map(|s| s.total_amount).collect();

chart
.draw_series(LineSeries::new(
years
.iter()
.zip(principal_and_contribution.iter())
.map(|(x, y)| (*x, *y)),
&RED,
))?
.label("Principal + Contribution")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 10, y)], &RED));

chart
.draw_series(LineSeries::new(
years.iter().zip(total_amount.iter()).map(|(x, y)| (*x, *y)),
&BLUE,
))?
.label("Total Amount")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 10, y)], &BLUE));

chart
.configure_series_labels()
.position(SeriesLabelPosition::UpperLeft)
.draw()?;

Ok(())
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod calculations;
72 changes: 72 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
mod calculations;

use calculations::{plot_summary, Investment};
use clap::{Arg, Command};
use serde_json::to_string_pretty;
use std::env;

fn run() {
let matches = Command::new("Compound Interest Calculator")
.about("cis - Calculates Compound Interest.\nOutput the results of compound interest calculations as either a line graph image or JSON.")
.arg(
Arg::new("principal")
.short('p')
.long("principal")
.value_name("PRINCIPAL")
.help("The principal at the time you started investing. Defaults to 0"),
)
.arg(
Arg::new("contribution")
.short('c')
.long("contribution")
.value_name("CONTRIBUTION")
.help("The monthly contribution amount. Defaults to 1"),
)
.arg(
Arg::new("rate")
.short('r')
.long("rate")
.value_name("RATE")
.help("The annual interest rate (in %). Defaults to 5"),
)
.arg(
Arg::new("years")
.short('y')
.long("years")
.value_name("YEARS")
.help("The number of years for contributions. Defaults to 5"),
)
.arg(
Arg::new("json")
.short('j')
.long("json")
.help("Output as JSON. Defaults to false")
.action(clap::ArgAction::SetTrue),
)
.get_matches();

let args: Vec<String> = env::args().collect();
if args.len() == 1 {
println!("`cls --help` for usage");
return;
}

let investment = Investment::from_matches(&matches);
// Display the yearly summary
let summary = investment.yearly_summary();
if matches.get_flag("json") {
match to_string_pretty(&summary) {
Ok(json) => println!("{}", json),
Err(e) => eprintln!("Failed to serialize to JSON: {}", e),
}
} else {
match plot_summary(&summary) {
Ok(_) => (),
Err(e) => eprintln!("Failed to plot summary: {}", e),
}
}
}

fn main() {
run();
}

0 comments on commit 9e4480e

Please sign in to comment.