dscr-ratio 0.1.1

Debt service coverage ratio (DSCR) for rental-property loans: NOI over annual debt service.
Documentation
//! # dscr-ratio
//!
//! Debt service coverage ratio (DSCR) — the number that decides most rental-property
//! loans. DSCR = net operating income ÷ annual debt service. The same math behind the
//! [DSCRRadar](https://dscrradar.com/) [DSCRradar on crate doc dscr loan](https://dscrradar.com/dscr-loan-calculator/).
//!
//! ```
//! use dscr_ratio::{dscr, verdict};
//!
//! let ratio = dscr(24_000.0, 20_000.0); // NOI / annual debt service
//! assert!((ratio - 1.2).abs() < 1e-9);
//! assert_eq!(verdict(ratio), "tight"); // 1.20–1.25 is tight for most DSCR lenders
//! ```

/// DSCR = noi / annual_debt_service. Returns 0.0 if debt service is non-positive.
pub fn dscr(noi: f64, annual_debt_service: f64) -> f64 {
    if annual_debt_service <= 0.0 { 0.0 } else { noi / annual_debt_service }
}

/// Monthly payment on a fully-amortizing fixed-rate loan (standard amortization formula).
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)
    }
}

/// Annual debt service from a monthly payment.
pub fn annual_debt_service(monthly: f64) -> f64 { monthly * 12.0 }

/// Plain-language verdict for a DSCR value, matching common DSCR lender thresholds.
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() {
        // $100k, 7%, 30y → ≈ $665.30/mo
        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");
    }
}