pub fn dscr(noi: f64, annual_debt_service: f64) -> f64 {
if annual_debt_service <= 0.0 { 0.0 } else { noi / annual_debt_service }
}
pub fn monthly_payment(principal: f64, annual_rate: f64, months: u32) -> f64 {
if months == 0 { return 0.0; }
let r = annual_rate / 12.0;
if r == 0.0 { principal / months as f64 }
else {
let n = months as f64;
principal * r * (1.0 + r).powf(n) / ((1.0 + r).powf(n) - 1.0)
}
}
pub fn annual_debt_service(monthly: f64) -> f64 { monthly * 12.0 }
pub fn verdict(ratio: f64) -> &'static str {
if ratio < 1.0 { "fail" }
else if ratio < 1.15 { "marginal" }
else if ratio < 1.25 { "tight" }
else if ratio < 1.35 { "good" }
else { "strong" }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_ratio() { assert!((dscr(24_000.0, 20_000.0) - 1.2).abs() < 1e-9); }
#[test]
fn break_even() { assert!((dscr(20_000.0, 20_000.0) - 1.0).abs() < 1e-9); }
#[test]
fn zero_debt_is_zero_ratio() { assert_eq!(dscr(100_000.0, 0.0), 0.0); }
#[test]
fn amort_payment() {
let m = monthly_payment(100_000.0, 0.07, 360);
assert!((m - 665.30).abs() < 0.1);
}
#[test]
fn verdict_buckets() {
assert_eq!(verdict(0.9), "fail");
assert_eq!(verdict(1.1), "marginal");
assert_eq!(verdict(1.2), "tight");
assert_eq!(verdict(1.3), "good");
assert_eq!(verdict(1.5), "strong");
}
}