|
| 1 | +//! Calculates simple, compound, and APR interest on a principal amount. |
| 2 | +//! |
| 3 | +//! Formulas: |
| 4 | +//! Simple Interest: I = p * r * t |
| 5 | +//! Compound Interest: I = p * ((1 + r)^n - 1) |
| 6 | +//! APR Interest: Compound interest with r = annual_rate / 365 |
| 7 | +//! and n = years * 365 |
| 8 | +//! where: |
| 9 | +//! - `p` is the principal |
| 10 | +//! - `r` is the interest rate per period |
| 11 | +//! - `t` is the number of periods (days) |
| 12 | +//! - `n` is the total number of compounding periods |
| 13 | +//! |
| 14 | +//! Reference: https://www.investopedia.com/terms/i/interest.asp |
| 15 | +
|
| 16 | +/// Calculates simple interest earned over a number of days. |
| 17 | +/// |
| 18 | +/// # Arguments |
| 19 | +/// * `principal` - The initial amount of money (must be > 0) |
| 20 | +/// * `daily_interest_rate` - The daily interest rate as a decimal (must be >= 0) |
| 21 | +/// * `days_between_payments` - The number of days between payments (must be > 0) |
| 22 | +/// |
| 23 | +/// # Errors |
| 24 | +/// Returns an `Err(&'static str)` if any argument is out of range. |
| 25 | +pub fn simple_interest( |
| 26 | + principal: f64, |
| 27 | + daily_interest_rate: f64, |
| 28 | + days_between_payments: f64, |
| 29 | +) -> Result<f64, &'static str> { |
| 30 | + if principal <= 0.0 { |
| 31 | + return Err("principal must be > 0"); |
| 32 | + } |
| 33 | + if daily_interest_rate < 0.0 { |
| 34 | + return Err("daily_interest_rate must be >= 0"); |
| 35 | + } |
| 36 | + if days_between_payments <= 0.0 { |
| 37 | + return Err("days_between_payments must be > 0"); |
| 38 | + } |
| 39 | + Ok(principal * daily_interest_rate * days_between_payments) |
| 40 | +} |
| 41 | + |
| 42 | +/// Calculates compound interest earned over a number of compounding periods. |
| 43 | +/// |
| 44 | +/// # Arguments |
| 45 | +/// * `principal` - The initial amount of money (must be > 0) |
| 46 | +/// * `nominal_annual_interest_rate` - The rate per compounding period as a decimal (must be >= 0) |
| 47 | +/// * `number_of_compounding_periods` - The total number of compounding periods (must be > 0) |
| 48 | +/// |
| 49 | +/// # Errors |
| 50 | +/// Returns an `Err(&'static str)` if any argument is out of range. |
| 51 | +pub fn compound_interest( |
| 52 | + principal: f64, |
| 53 | + nominal_annual_interest_rate: f64, |
| 54 | + number_of_compounding_periods: f64, |
| 55 | +) -> Result<f64, &'static str> { |
| 56 | + if principal <= 0.0 { |
| 57 | + return Err("principal must be > 0"); |
| 58 | + } |
| 59 | + if nominal_annual_interest_rate < 0.0 { |
| 60 | + return Err("nominal_annual_interest_rate must be >= 0"); |
| 61 | + } |
| 62 | + if number_of_compounding_periods <= 0.0 { |
| 63 | + return Err("number_of_compounding_periods must be > 0"); |
| 64 | + } |
| 65 | + Ok( |
| 66 | + principal |
| 67 | + * ((1.0 + nominal_annual_interest_rate).powf(number_of_compounding_periods) - 1.0), |
| 68 | + ) |
| 69 | +} |
| 70 | + |
| 71 | +/// Calculates interest using the Annual Percentage Rate (APR), compounded daily. |
| 72 | +/// |
| 73 | +/// Converts the APR to a daily rate and compounds over the equivalent number |
| 74 | +/// of days, giving a more accurate real-world figure than simple annual compounding. |
| 75 | +/// |
| 76 | +/// # Arguments |
| 77 | +/// * `principal` - The initial amount of money (must be > 0) |
| 78 | +/// * `nominal_annual_percentage_rate` - The APR as a decimal (must be >= 0) |
| 79 | +/// * `number_of_years` - The loan or investment duration in years (must be > 0) |
| 80 | +/// |
| 81 | +/// # Errors |
| 82 | +/// Returns an `Err(&'static str)` if any argument is out of range. |
| 83 | +pub fn apr_interest( |
| 84 | + principal: f64, |
| 85 | + nominal_annual_percentage_rate: f64, |
| 86 | + number_of_years: f64, |
| 87 | +) -> Result<f64, &'static str> { |
| 88 | + if principal <= 0.0 { |
| 89 | + return Err("principal must be > 0"); |
| 90 | + } |
| 91 | + if nominal_annual_percentage_rate < 0.0 { |
| 92 | + return Err("nominal_annual_percentage_rate must be >= 0"); |
| 93 | + } |
| 94 | + if number_of_years <= 0.0 { |
| 95 | + return Err("number_of_years must be > 0"); |
| 96 | + } |
| 97 | + // Divide annual rate by 365 to obtain the daily rate |
| 98 | + // Multiply years by 365 to obtain the total number of daily compounding periods |
| 99 | + compound_interest( |
| 100 | + principal, |
| 101 | + nominal_annual_percentage_rate / 365.0, |
| 102 | + number_of_years * 365.0, |
| 103 | + ) |
| 104 | +} |
| 105 | + |
| 106 | +#[cfg(test)] |
| 107 | +mod tests { |
| 108 | + use super::*; |
| 109 | + |
| 110 | + #[test] |
| 111 | + fn test_simple_interest() { |
| 112 | + const EPSILON: f64 = 1e-9; |
| 113 | + |
| 114 | + // Standard cases |
| 115 | + assert!((simple_interest(18000.0, 0.06, 3.0).unwrap() - 3240.0).abs() < EPSILON); |
| 116 | + assert!((simple_interest(0.5, 0.06, 3.0).unwrap() - 0.09).abs() < EPSILON); |
| 117 | + assert!((simple_interest(18000.0, 0.01, 10.0).unwrap() - 1800.0).abs() < EPSILON); |
| 118 | + assert!((simple_interest(5500.0, 0.01, 100.0).unwrap() - 5500.0).abs() < EPSILON); |
| 119 | + |
| 120 | + // Zero interest rate yields zero interest |
| 121 | + assert!((simple_interest(18000.0, 0.0, 3.0).unwrap() - 0.0).abs() < EPSILON); |
| 122 | + |
| 123 | + // Error cases |
| 124 | + assert_eq!( |
| 125 | + simple_interest(-10000.0, 0.06, 3.0), |
| 126 | + Err("principal must be > 0") |
| 127 | + ); |
| 128 | + assert_eq!( |
| 129 | + simple_interest(0.0, 0.06, 3.0), |
| 130 | + Err("principal must be > 0") |
| 131 | + ); |
| 132 | + assert_eq!( |
| 133 | + simple_interest(10000.0, -0.06, 3.0), |
| 134 | + Err("daily_interest_rate must be >= 0") |
| 135 | + ); |
| 136 | + assert_eq!( |
| 137 | + simple_interest(5500.0, 0.01, -5.0), |
| 138 | + Err("days_between_payments must be > 0") |
| 139 | + ); |
| 140 | + assert_eq!( |
| 141 | + simple_interest(5500.0, 0.01, 0.0), |
| 142 | + Err("days_between_payments must be > 0") |
| 143 | + ); |
| 144 | + } |
| 145 | + |
| 146 | + #[test] |
| 147 | + fn test_compound_interest() { |
| 148 | + const EPSILON: f64 = 1e-9; |
| 149 | + |
| 150 | + // Standard cases |
| 151 | + assert!( |
| 152 | + (compound_interest(10000.0, 0.05, 3.0).unwrap() - 1_576.250_000_000_001_4).abs() |
| 153 | + < EPSILON |
| 154 | + ); |
| 155 | + assert!( |
| 156 | + (compound_interest(10000.0, 0.05, 1.0).unwrap() - 500.000_000_000_000_45).abs() |
| 157 | + < EPSILON |
| 158 | + ); |
| 159 | + assert!( |
| 160 | + (compound_interest(0.5, 0.05, 3.0).unwrap() - 0.078_812_500_000_000_06).abs() < EPSILON |
| 161 | + ); |
| 162 | + |
| 163 | + // Zero interest rate yields zero interest |
| 164 | + assert!((compound_interest(10000.0, 0.0, 5.0).unwrap() - 0.0).abs() < EPSILON); |
| 165 | + |
| 166 | + // Error cases |
| 167 | + assert_eq!( |
| 168 | + compound_interest(-5500.0, 0.01, 5.0), |
| 169 | + Err("principal must be > 0") |
| 170 | + ); |
| 171 | + assert_eq!( |
| 172 | + compound_interest(10000.0, -3.5, 3.0), |
| 173 | + Err("nominal_annual_interest_rate must be >= 0") |
| 174 | + ); |
| 175 | + assert_eq!( |
| 176 | + compound_interest(10000.0, 0.06, -4.0), |
| 177 | + Err("number_of_compounding_periods must be > 0") |
| 178 | + ); |
| 179 | + } |
| 180 | + |
| 181 | + #[test] |
| 182 | + fn test_apr_interest() { |
| 183 | + const EPSILON: f64 = 1e-9; |
| 184 | + |
| 185 | + // Standard cases |
| 186 | + assert!( |
| 187 | + (apr_interest(10000.0, 0.05, 3.0).unwrap() - 1_618.223_072_263_547).abs() < EPSILON |
| 188 | + ); |
| 189 | + assert!( |
| 190 | + (apr_interest(10000.0, 0.05, 1.0).unwrap() - 512.674_964_674_473_2).abs() < EPSILON |
| 191 | + ); |
| 192 | + assert!((apr_interest(0.5, 0.05, 3.0).unwrap() - 0.080_911_153_613_177_36).abs() < EPSILON); |
| 193 | + |
| 194 | + // Zero interest rate yields zero interest |
| 195 | + assert!((apr_interest(10000.0, 0.0, 5.0).unwrap() - 0.0).abs() < EPSILON); |
| 196 | + |
| 197 | + // Error cases |
| 198 | + assert_eq!( |
| 199 | + apr_interest(-5500.0, 0.01, 5.0), |
| 200 | + Err("principal must be > 0") |
| 201 | + ); |
| 202 | + assert_eq!( |
| 203 | + apr_interest(10000.0, -3.5, 3.0), |
| 204 | + Err("nominal_annual_percentage_rate must be >= 0") |
| 205 | + ); |
| 206 | + assert_eq!( |
| 207 | + apr_interest(10000.0, 0.06, -4.0), |
| 208 | + Err("number_of_years must be > 0") |
| 209 | + ); |
| 210 | + } |
| 211 | +} |
0 commit comments