comparative-fault 0.1.0

Apply US comparative-negligence rules (pure, modified 50%, modified 51%, contributory) to a damages award.
Documentation
//! Apply US comparative-negligence rules to a damages award.
//!
//! When more than one party is at fault for an injury, the plaintiff's recovery
//! is reduced by their own share of responsibility. US states do this in four
//! materially different ways, and the same facts can produce very different
//! recoveries depending on which rule applies:
//!
//! - **Pure comparative**: recovery is reduced by the plaintiff's fault share,
//!   with no cut-off. A plaintiff 90% at fault still recovers 10%.
//! - **Modified 50% bar**: the reduction applies, but recovery is barred once
//!   the plaintiff's share reaches 50%.
//! - **Modified 51% bar**: the same, but barred only once the share exceeds
//!   50% (i.e. at 51% or more).
//! - **Contributory negligence**: any fault at all bars recovery entirely.
//!   Only a small minority of jurisdictions still use this.
//!
//! The distinction between the two modified rules matters precisely at an even
//! 50/50 split, which is a common settlement posture: under a 50% bar the
//! plaintiff takes nothing, under a 51% bar they take half.
//!
//! The reference settlement calculator and worked examples are at
//! <https://worthmyclaim.com/> — an injury settlement estimator.
//!
//! # Example
//!
//! ```
//! use comparative_fault::{net_recovery, Rule};
//!
//! // 20% at fault on a $100k award, pure comparative: $80k.
//! assert_eq!(net_recovery(100_000.0, 0.20, Rule::Pure), 80_000.0);
//!
//! // Exactly 50% at fault: barred under a 50% rule, halved under a 51% rule.
//! assert_eq!(net_recovery(100_000.0, 0.50, Rule::Modified50), 0.0);
//! assert_eq!(net_recovery(100_000.0, 0.50, Rule::Modified51), 50_000.0);
//! ```

/// The comparative-negligence regime applied by a jurisdiction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rule {
    /// Reduce by fault share with no cut-off.
    Pure,
    /// Reduce by fault share; barred at 50% or more.
    Modified50,
    /// Reduce by fault share; barred above 50%.
    Modified51,
    /// Any plaintiff fault bars recovery entirely.
    Contributory,
}

impl Rule {
    /// Returns true if `fault` (0.0..=1.0) bars recovery under this rule.
    pub fn bars_recovery(self, fault: f64) -> bool {
        match self {
            Rule::Pure => false,
            Rule::Modified50 => fault >= 0.50,
            Rule::Modified51 => fault > 0.50,
            Rule::Contributory => fault > 0.0,
        }
    }
}

/// Net recovery after applying the plaintiff's fault share under `rule`.
///
/// `gross` is the total damages figure before any reduction. `fault` is the
/// plaintiff's share of responsibility as a fraction in `0.0..=1.0`; values
/// outside that range are clamped. Returns `0.0` if recovery is barred.
pub fn net_recovery(gross: f64, fault: f64, rule: Rule) -> f64 {
    if gross <= 0.0 {
        return 0.0;
    }
    let fault = fault.clamp(0.0, 1.0);
    if rule.bars_recovery(fault) {
        return 0.0;
    }
    gross * (1.0 - fault)
}

/// The dollar amount lost to the plaintiff's own fault share.
pub fn reduction_amount(gross: f64, fault: f64, rule: Rule) -> f64 {
    if gross <= 0.0 {
        return 0.0;
    }
    gross - net_recovery(gross, fault, rule)
}

/// The fault share at which recovery first becomes zero, if the rule has one.
///
/// Returns `None` for [`Rule::Pure`], which has no cut-off.
pub fn bar_threshold(rule: Rule) -> Option<f64> {
    match rule {
        Rule::Pure => None,
        Rule::Modified50 => Some(0.50),
        Rule::Modified51 => Some(0.5000001),
        Rule::Contributory => Some(0.0),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn pure_has_no_cutoff() {
        assert!((net_recovery(100_000.0, 0.90, Rule::Pure) - 10_000.0).abs() < 1e-6);
        assert!(!Rule::Pure.bars_recovery(0.99));
    }

    #[test]
    fn modified_rules_differ_at_exactly_fifty() {
        assert_eq!(net_recovery(100_000.0, 0.50, Rule::Modified50), 0.0);
        assert!((net_recovery(100_000.0, 0.50, Rule::Modified51) - 50_000.0).abs() < 1e-6);
    }

    #[test]
    fn modified_51_bars_above_half() {
        assert_eq!(net_recovery(100_000.0, 0.51, Rule::Modified51), 0.0);
        assert!((net_recovery(100_000.0, 0.49, Rule::Modified51) - 51_000.0).abs() < 1e-6);
    }

    #[test]
    fn contributory_bars_any_fault() {
        assert_eq!(net_recovery(100_000.0, 0.01, Rule::Contributory), 0.0);
        assert_eq!(net_recovery(100_000.0, 0.0, Rule::Contributory), 100_000.0);
    }

    #[test]
    fn reduction_complements_recovery() {
        let g = 250_000.0;
        for f in [0.0, 0.1, 0.25, 0.49] {
            let net = net_recovery(g, f, Rule::Modified51);
            assert!((net + reduction_amount(g, f, Rule::Modified51) - g).abs() < 1e-6);
        }
    }

    #[test]
    fn clamps_and_guards() {
        assert_eq!(net_recovery(0.0, 0.2, Rule::Pure), 0.0);
        assert_eq!(net_recovery(-5.0, 0.2, Rule::Pure), 0.0);
        assert_eq!(net_recovery(100.0, -1.0, Rule::Pure), 100.0);
        assert_eq!(net_recovery(100.0, 5.0, Rule::Pure), 0.0);
    }

    #[test]
    fn thresholds() {
        assert_eq!(bar_threshold(Rule::Pure), None);
        assert_eq!(bar_threshold(Rule::Modified50), Some(0.50));
        assert_eq!(bar_threshold(Rule::Contributory), Some(0.0));
    }
}