|
| 1 | +//! Calculates the Equated Monthly Installment (EMI) for a loan. |
| 2 | +//! |
| 3 | +//! Formula: A = p * r * (1 + r)^n / ((1 + r)^n - 1) |
| 4 | +//! where: |
| 5 | +//! - `p` is the principal |
| 6 | +//! - `r` is the monthly interest rate (annual rate / 12) |
| 7 | +//! - `n` is the total number of monthly payments (years * 12) |
| 8 | +//! |
| 9 | +//! Wikipedia Reference: https://en.wikipedia.org/wiki/Equated_monthly_installment |
| 10 | +
|
| 11 | +/// Computes the monthly EMI for a loan. |
| 12 | +/// |
| 13 | +/// # Arguments |
| 14 | +/// * `principal` - The total amount borrowed (must be > 0) |
| 15 | +/// * `rate_per_annum` - Annual interest rate as a decimal, e.g. 0.12 for 12% (must be >= 0) |
| 16 | +/// * `years_to_repay` - Loan term in whole years (must be > 0) |
| 17 | +/// |
| 18 | +/// # Errors |
| 19 | +/// Returns an `Err(&'static str)` if any argument is out of range. |
| 20 | +pub fn equated_monthly_installments( |
| 21 | + principal: f64, |
| 22 | + rate_per_annum: f64, |
| 23 | + years_to_repay: u32, |
| 24 | +) -> Result<f64, &'static str> { |
| 25 | + if principal <= 0.0 { |
| 26 | + return Err("Principal borrowed must be > 0"); |
| 27 | + } |
| 28 | + if rate_per_annum < 0.0 { |
| 29 | + return Err("Rate of interest must be >= 0"); |
| 30 | + } |
| 31 | + if years_to_repay == 0 { |
| 32 | + return Err("Years to repay must be an integer > 0"); |
| 33 | + } |
| 34 | + |
| 35 | + // Divide annual rate by 12 to obtain the monthly rate |
| 36 | + let rate_per_month = rate_per_annum / 12.0; |
| 37 | + |
| 38 | + // Multiply years by 12 to obtain the total number of monthly payments |
| 39 | + let number_of_payments = f64::from(years_to_repay * 12); |
| 40 | + |
| 41 | + // Handle the edge case where the interest rate is 0 (simple division) |
| 42 | + if rate_per_month == 0.0 { |
| 43 | + return Ok(principal / number_of_payments); |
| 44 | + } |
| 45 | + |
| 46 | + let factor = (1.0 + rate_per_month).powf(number_of_payments); |
| 47 | + Ok(principal * rate_per_month * factor / (factor - 1.0)) |
| 48 | +} |
| 49 | + |
| 50 | +#[cfg(test)] |
| 51 | +mod tests { |
| 52 | + use super::*; |
| 53 | + |
| 54 | + #[test] |
| 55 | + fn test_equated_monthly_installments() { |
| 56 | + const EPSILON: f64 = 1e-8; |
| 57 | + |
| 58 | + // Standard cases |
| 59 | + let result = equated_monthly_installments(25000.0, 0.12, 3).unwrap(); |
| 60 | + assert!((result - 830.357_745_321_279_3).abs() < EPSILON); |
| 61 | + |
| 62 | + let result = equated_monthly_installments(25000.0, 0.12, 10).unwrap(); |
| 63 | + assert!((result - 358.677_371_006_468_26).abs() < EPSILON); |
| 64 | + |
| 65 | + // With 0% interest the EMI is simply principal / number_of_payments |
| 66 | + let result = equated_monthly_installments(12000.0, 0.0, 1).unwrap(); |
| 67 | + assert!((result - 1000.0).abs() < EPSILON); |
| 68 | + |
| 69 | + // Error cases |
| 70 | + assert_eq!( |
| 71 | + equated_monthly_installments(0.0, 0.12, 3), |
| 72 | + Err("Principal borrowed must be > 0") |
| 73 | + ); |
| 74 | + assert_eq!( |
| 75 | + equated_monthly_installments(-5000.0, 0.12, 3), |
| 76 | + Err("Principal borrowed must be > 0") |
| 77 | + ); |
| 78 | + assert_eq!( |
| 79 | + equated_monthly_installments(25000.0, -1.0, 3), |
| 80 | + Err("Rate of interest must be >= 0") |
| 81 | + ); |
| 82 | + assert_eq!( |
| 83 | + equated_monthly_installments(25000.0, 0.12, 0), |
| 84 | + Err("Years to repay must be an integer > 0") |
| 85 | + ); |
| 86 | + } |
| 87 | +} |
0 commit comments