-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
388 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,3 +12,4 @@ Cargo.lock | |
|
||
# MSVC Windows builds of rustc generate these, which store debugging information | ||
*.pdb | ||
/plot.png |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub mod calculations; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |