1pub fn dscr(noi: f64, annual_debt_service: f64) -> f64 {
17 if annual_debt_service <= 0.0 { 0.0 } else { noi / annual_debt_service }
18}
19
20pub fn monthly_payment(principal: f64, annual_rate: f64, months: u32) -> f64 {
22 if months == 0 { return 0.0; }
23 let r = annual_rate / 12.0;
24 if r == 0.0 { principal / months as f64 }
25 else {
26 let n = months as f64;
27 principal * r * (1.0 + r).powf(n) / ((1.0 + r).powf(n) - 1.0)
28 }
29}
30
31pub fn annual_debt_service(monthly: f64) -> f64 { monthly * 12.0 }
33
34pub fn verdict(ratio: f64) -> &'static str {
36 if ratio < 1.0 { "fail" }
37 else if ratio < 1.15 { "marginal" }
38 else if ratio < 1.25 { "tight" }
39 else if ratio < 1.35 { "good" }
40 else { "strong" }
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46 #[test]
47 fn basic_ratio() { assert!((dscr(24_000.0, 20_000.0) - 1.2).abs() < 1e-9); }
48 #[test]
49 fn break_even() { assert!((dscr(20_000.0, 20_000.0) - 1.0).abs() < 1e-9); }
50 #[test]
51 fn zero_debt_is_zero_ratio() { assert_eq!(dscr(100_000.0, 0.0), 0.0); }
52 #[test]
53 fn amort_payment() {
54 let m = monthly_payment(100_000.0, 0.07, 360);
56 assert!((m - 665.30).abs() < 0.1);
57 }
58 #[test]
59 fn verdict_buckets() {
60 assert_eq!(verdict(0.9), "fail");
61 assert_eq!(verdict(1.1), "marginal");
62 assert_eq!(verdict(1.2), "tight");
63 assert_eq!(verdict(1.3), "good");
64 assert_eq!(verdict(1.5), "strong");
65 }
66}