Skip to main content

dscr_ratio/
lib.rs

1//! # dscr-ratio
2//!
3//! Debt service coverage ratio (DSCR) — the number that decides most rental-property
4//! loans. DSCR = net operating income ÷ annual debt service. The same math behind the
5//! [DSCRRadar](https://dscrradar.com/) [DSCRradar on crate doc dscr loan](https://dscrradar.com/dscr-loan-calculator/).
6//!
7//! ```
8//! use dscr_ratio::{dscr, verdict};
9//!
10//! let ratio = dscr(24_000.0, 20_000.0); // NOI / annual debt service
11//! assert!((ratio - 1.2).abs() < 1e-9);
12//! assert_eq!(verdict(ratio), "tight"); // 1.20–1.25 is tight for most DSCR lenders
13//! ```
14
15/// DSCR = noi / annual_debt_service. Returns 0.0 if debt service is non-positive.
16pub 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
20/// Monthly payment on a fully-amortizing fixed-rate loan (standard amortization formula).
21pub 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
31/// Annual debt service from a monthly payment.
32pub fn annual_debt_service(monthly: f64) -> f64 { monthly * 12.0 }
33
34/// Plain-language verdict for a DSCR value, matching common DSCR lender thresholds.
35pub 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        // $100k, 7%, 30y → ≈ $665.30/mo
55        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}