From 9e4480ee75fbbc5432abd935bd32ae456067d4af Mon Sep 17 00:00:00 2001 From: CHIKAMATSU Naohiro Date: Fri, 19 Jul 2024 23:38:09 +0900 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.toml | 17 ++++ README.md | 99 +++++++++++++++++++++- doc/image/plot.png | Bin 0 -> 57800 bytes src/calculations.rs | 200 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 72 ++++++++++++++++ 7 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 Cargo.toml create mode 100644 doc/image/plot.png create mode 100644 src/calculations.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index 6985cf1..b20bc15 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +/plot.png diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..89cc7ad --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cic" +version = "0.1.0" +edition = "2021" +authors = ["Naohiro CHIKAMATSU "] +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" diff --git a/README.md b/README.md index d63fc00..826a4bc 100644 --- a/README.md +++ b/README.md @@ -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 The principal at the time you started investing. Defaults to 0 + -c, --contribution The monthly contribution amount. Defaults to 1 + -r, --rate The annual interest rate (in %). Defaults to 5 + -y, --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 + diff --git a/doc/image/plot.png b/doc/image/plot.png new file mode 100644 index 0000000000000000000000000000000000000000..0d0357c0c9ac04c5e821fb9ec18cca52b33b5306 GIT binary patch literal 57800 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 J>uc|B`U~o2b&UW3 literal 0 HcmV?d00001 diff --git a/src/calculations.rs b/src/calculations.rs new file mode 100644 index 0000000..6368de6 --- /dev/null +++ b/src/calculations.rs @@ -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::("principal") + .and_then(|s| s.parse().ok()) + .unwrap_or(0.0), + contribution: matches + .get_one::("contribution") + .and_then(|s| s.parse().ok()) + .unwrap_or(1.0), + rate: matches + .get_one::("rate") + .and_then(|s| s.parse().ok()) + .unwrap_or(5.0), + years: matches + .get_one::("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 { + 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>` 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> { + 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 = summary.iter().map(|s| s.year as usize).collect(); + let mut principal_and_contribution: Vec = 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 = 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(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..af130d1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod calculations; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..35a358f --- /dev/null +++ b/src/main.rs @@ -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 = 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(); +}