use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct CompoundInterestParams {
pub principal: f64,
pub annual_rate: f64,
pub compounds_per_year: u32,
pub years: f64,
}
#[derive(Debug, Clone)]
pub struct CompoundInterestResult {
pub final_amount: f64,
pub total_interest: f64,
pub principal: f64,
pub effective_annual_rate: f64,
}
pub fn calculate_compound_interest(params: &CompoundInterestParams) -> CompoundInterestResult {
let principal = params.principal;
let rate = params.annual_rate;
let compounds = params.compounds_per_year as f64;
let years = params.years;
let final_amount = principal * (1.0 + rate / compounds).powf(compounds * years);
let total_interest = final_amount - principal;
let effective_annual_rate = (1.0 + rate / compounds).powf(compounds) - 1.0;
CompoundInterestResult {
final_amount,
total_interest,
principal,
effective_annual_rate,
}
}
pub fn calculate_compound_interest_with_contributions(
params: &CompoundInterestParams,
monthly_contribution: f64,
) -> CompoundInterestResult {
let principal = params.principal;
let rate = params.annual_rate;
let compounds = params.compounds_per_year as f64;
let years = params.years;
let monthly_rate = rate / 12.0;
let total_months = years * 12.0;
let principal_future_value = principal * (1.0 + rate / compounds).powf(compounds * years);
let contribution_future_value = if monthly_rate > 0.0 {
monthly_contribution * ((1.0 + monthly_rate).powf(total_months) - 1.0) / monthly_rate
} else {
monthly_contribution * total_months
};
let final_amount = principal_future_value + contribution_future_value;
let total_interest = final_amount - principal - (monthly_contribution * total_months);
let effective_annual_rate = (1.0 + rate / compounds).powf(compounds) - 1.0;
CompoundInterestResult {
final_amount,
total_interest,
principal,
effective_annual_rate,
}
}
pub fn calculate_time_to_target(
principal: f64,
target_amount: f64,
annual_rate: f64,
compounds_per_year: u32,
) -> f64 {
let rate = annual_rate;
let compounds = compounds_per_year as f64;
if rate <= 0.0 || principal <= 0.0 || target_amount <= principal {
return 0.0;
}
let years = (target_amount / principal).ln() / (compounds * (1.0 + rate / compounds).ln());
years
}
pub fn calculate_principal_for_target(
target_amount: f64,
annual_rate: f64,
compounds_per_year: u32,
years: f64,
) -> f64 {
let rate = annual_rate;
let compounds = compounds_per_year as f64;
if rate <= 0.0 || years <= 0.0 {
return 0.0;
}
let principal = target_amount / (1.0 + rate / compounds).powf(compounds * years);
principal
}
pub fn generate_breakdown(params: &CompoundInterestParams) -> HashMap<u32, CompoundInterestResult> {
let mut breakdown = HashMap::new();
for year in 1..=(params.years as u32) {
let year_params = CompoundInterestParams {
years: year as f64,
..params.clone()
};
breakdown.insert(year, calculate_compound_interest(&year_params));
}
breakdown
}
pub fn format_currency(amount: f64) -> String {
let is_negative = amount < 0.0;
let abs_amount = amount.abs();
let formatted = format!("{:.2}", abs_amount);
let parts: Vec<&str> = formatted.split('.').collect();
let integer_part = parts[0];
let decimal_part = if parts.len() > 1 { parts[1] } else { "00" };
let mut result = String::new();
for (i, ch) in integer_part.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(',');
}
result.push(ch);
}
let integer_with_commas: String = result.chars().rev().collect();
let sign = if is_negative { "-" } else { "" };
format!("${}{}.{}", sign, integer_with_commas, decimal_part)
}
pub fn format_percentage(rate: f64) -> String {
format!("{:.2}%", rate * 100.0)
}
pub fn calculate_weekly_with_yearly_tax(
principal: f64,
weekly_rate: f64,
weeks: u32,
weekly_contribution: f64,
capital_gains_tax: f64,
) -> (f64, f64, f64) {
let weeks_per_year = 52;
let years = weeks / weeks_per_year;
let remaining_weeks = weeks % weeks_per_year;
let mut current_principal = principal;
let mut total_tax_paid = 0.0;
let mut total_contributions = 0.0;
for _year in 0..years {
let year_start_principal = current_principal;
let year_contributions = weekly_contribution * weeks_per_year as f64;
total_contributions += year_contributions;
let year_end_principal = year_start_principal * (1.0 + weekly_rate).powf(weeks_per_year as f64);
let year_end_contributions = if weekly_rate > 0.0 {
weekly_contribution * ((1.0 + weekly_rate).powf(weeks_per_year as f64) - 1.0) / weekly_rate
} else {
year_contributions
};
let year_end_total = year_end_principal + year_end_contributions;
let year_profit = year_end_total - year_start_principal - year_contributions;
let year_tax = if year_profit > 0.0 { year_profit * capital_gains_tax } else { 0.0 };
total_tax_paid += year_tax;
current_principal = year_end_total - year_tax;
}
if remaining_weeks > 0 {
let remaining_contributions = weekly_contribution * remaining_weeks as f64;
total_contributions += remaining_contributions;
let final_principal = current_principal * (1.0 + weekly_rate).powf(remaining_weeks as f64);
let final_contributions = if weekly_rate > 0.0 {
weekly_contribution * ((1.0 + weekly_rate).powf(remaining_weeks as f64) - 1.0) / weekly_rate
} else {
remaining_contributions
};
let final_total = final_principal + final_contributions;
let remaining_profit = final_total - current_principal - remaining_contributions;
let remaining_tax = if remaining_profit > 0.0 {
remaining_profit * capital_gains_tax * (remaining_weeks as f64 / weeks_per_year as f64)
} else {
0.0
};
total_tax_paid += remaining_tax;
current_principal = final_total - remaining_tax;
}
let total_profit_before_tax = current_principal + total_tax_paid - principal - total_contributions;
(current_principal, total_profit_before_tax, total_tax_paid)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_compound_interest() {
let params = CompoundInterestParams {
principal: 1000.0,
annual_rate: 0.05,
compounds_per_year: 1,
years: 10.0,
};
let result = calculate_compound_interest(¶ms);
assert!((result.final_amount - 1628.89).abs() < 0.01);
assert!((result.total_interest - 628.89).abs() < 0.01);
}
#[test]
fn test_monthly_compounding() {
let params = CompoundInterestParams {
principal: 1000.0,
annual_rate: 0.05,
compounds_per_year: 12,
years: 1.0,
};
let result = calculate_compound_interest(¶ms);
assert!(result.final_amount > 1050.0);
}
#[test]
fn test_compound_interest_with_contributions() {
let params = CompoundInterestParams {
principal: 1000.0,
annual_rate: 0.05,
compounds_per_year: 12,
years: 10.0,
};
let result = calculate_compound_interest_with_contributions(¶ms, 100.0);
let result_no_contributions = calculate_compound_interest(¶ms);
assert!(result.final_amount > result_no_contributions.final_amount);
}
#[test]
fn test_time_to_target() {
let years = calculate_time_to_target(1000.0, 2000.0, 0.05, 1);
assert!((years - 14.2).abs() < 0.5);
}
#[test]
fn test_principal_for_target() {
let principal = calculate_principal_for_target(2000.0, 0.05, 1, 10.0);
assert!((principal - 1227.83).abs() < 1.0);
}
#[test]
fn test_weekly_with_tax() {
let principal = 10000.0;
let weekly_rate = 0.01; let weeks = 52 * 2; let weekly_contribution = 100.0;
let capital_gains_tax = 0.3;
let (final_after_tax, profit, tax_paid) = calculate_weekly_with_yearly_tax(
principal,
weekly_rate,
weeks,
weekly_contribution,
capital_gains_tax,
);
let (final_no_tax, profit_no_tax, _) = calculate_weekly_with_yearly_tax(
principal,
weekly_rate,
weeks,
weekly_contribution,
0.0,
);
assert!(final_after_tax < final_no_tax);
assert!(final_after_tax < final_no_tax);
let total_contributions = weekly_contribution * weeks as f64;
assert!(final_after_tax > principal + total_contributions);
}
}