Skip to main content

comparative_fault/
lib.rs

1//! Apply US comparative-negligence rules to a damages award.
2//!
3//! When more than one party is at fault for an injury, the plaintiff's recovery
4//! is reduced by their own share of responsibility. US states do this in four
5//! materially different ways, and the same facts can produce very different
6//! recoveries depending on which rule applies:
7//!
8//! - **Pure comparative**: recovery is reduced by the plaintiff's fault share,
9//!   with no cut-off. A plaintiff 90% at fault still recovers 10%.
10//! - **Modified 50% bar**: the reduction applies, but recovery is barred once
11//!   the plaintiff's share reaches 50%.
12//! - **Modified 51% bar**: the same, but barred only once the share exceeds
13//!   50% (i.e. at 51% or more).
14//! - **Contributory negligence**: any fault at all bars recovery entirely.
15//!   Only a small minority of jurisdictions still use this.
16//!
17//! The distinction between the two modified rules matters precisely at an even
18//! 50/50 split, which is a common settlement posture: under a 50% bar the
19//! plaintiff takes nothing, under a 51% bar they take half.
20//!
21//! The reference settlement calculator and worked examples are at
22//! <https://worthmyclaim.com/> — an injury settlement estimator.
23//!
24//! # Example
25//!
26//! ```
27//! use comparative_fault::{net_recovery, Rule};
28//!
29//! // 20% at fault on a $100k award, pure comparative: $80k.
30//! assert_eq!(net_recovery(100_000.0, 0.20, Rule::Pure), 80_000.0);
31//!
32//! // Exactly 50% at fault: barred under a 50% rule, halved under a 51% rule.
33//! assert_eq!(net_recovery(100_000.0, 0.50, Rule::Modified50), 0.0);
34//! assert_eq!(net_recovery(100_000.0, 0.50, Rule::Modified51), 50_000.0);
35//! ```
36
37/// The comparative-negligence regime applied by a jurisdiction.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Rule {
40    /// Reduce by fault share with no cut-off.
41    Pure,
42    /// Reduce by fault share; barred at 50% or more.
43    Modified50,
44    /// Reduce by fault share; barred above 50%.
45    Modified51,
46    /// Any plaintiff fault bars recovery entirely.
47    Contributory,
48}
49
50impl Rule {
51    /// Returns true if `fault` (0.0..=1.0) bars recovery under this rule.
52    pub fn bars_recovery(self, fault: f64) -> bool {
53        match self {
54            Rule::Pure => false,
55            Rule::Modified50 => fault >= 0.50,
56            Rule::Modified51 => fault > 0.50,
57            Rule::Contributory => fault > 0.0,
58        }
59    }
60}
61
62/// Net recovery after applying the plaintiff's fault share under `rule`.
63///
64/// `gross` is the total damages figure before any reduction. `fault` is the
65/// plaintiff's share of responsibility as a fraction in `0.0..=1.0`; values
66/// outside that range are clamped. Returns `0.0` if recovery is barred.
67pub fn net_recovery(gross: f64, fault: f64, rule: Rule) -> f64 {
68    if gross <= 0.0 {
69        return 0.0;
70    }
71    let fault = fault.clamp(0.0, 1.0);
72    if rule.bars_recovery(fault) {
73        return 0.0;
74    }
75    gross * (1.0 - fault)
76}
77
78/// The dollar amount lost to the plaintiff's own fault share.
79pub fn reduction_amount(gross: f64, fault: f64, rule: Rule) -> f64 {
80    if gross <= 0.0 {
81        return 0.0;
82    }
83    gross - net_recovery(gross, fault, rule)
84}
85
86/// The fault share at which recovery first becomes zero, if the rule has one.
87///
88/// Returns `None` for [`Rule::Pure`], which has no cut-off.
89pub fn bar_threshold(rule: Rule) -> Option<f64> {
90    match rule {
91        Rule::Pure => None,
92        Rule::Modified50 => Some(0.50),
93        Rule::Modified51 => Some(0.5000001),
94        Rule::Contributory => Some(0.0),
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn pure_has_no_cutoff() {
104        assert!((net_recovery(100_000.0, 0.90, Rule::Pure) - 10_000.0).abs() < 1e-6);
105        assert!(!Rule::Pure.bars_recovery(0.99));
106    }
107
108    #[test]
109    fn modified_rules_differ_at_exactly_fifty() {
110        assert_eq!(net_recovery(100_000.0, 0.50, Rule::Modified50), 0.0);
111        assert!((net_recovery(100_000.0, 0.50, Rule::Modified51) - 50_000.0).abs() < 1e-6);
112    }
113
114    #[test]
115    fn modified_51_bars_above_half() {
116        assert_eq!(net_recovery(100_000.0, 0.51, Rule::Modified51), 0.0);
117        assert!((net_recovery(100_000.0, 0.49, Rule::Modified51) - 51_000.0).abs() < 1e-6);
118    }
119
120    #[test]
121    fn contributory_bars_any_fault() {
122        assert_eq!(net_recovery(100_000.0, 0.01, Rule::Contributory), 0.0);
123        assert_eq!(net_recovery(100_000.0, 0.0, Rule::Contributory), 100_000.0);
124    }
125
126    #[test]
127    fn reduction_complements_recovery() {
128        let g = 250_000.0;
129        for f in [0.0, 0.1, 0.25, 0.49] {
130            let net = net_recovery(g, f, Rule::Modified51);
131            assert!((net + reduction_amount(g, f, Rule::Modified51) - g).abs() < 1e-6);
132        }
133    }
134
135    #[test]
136    fn clamps_and_guards() {
137        assert_eq!(net_recovery(0.0, 0.2, Rule::Pure), 0.0);
138        assert_eq!(net_recovery(-5.0, 0.2, Rule::Pure), 0.0);
139        assert_eq!(net_recovery(100.0, -1.0, Rule::Pure), 100.0);
140        assert_eq!(net_recovery(100.0, 5.0, Rule::Pure), 0.0);
141    }
142
143    #[test]
144    fn thresholds() {
145        assert_eq!(bar_threshold(Rule::Pure), None);
146        assert_eq!(bar_threshold(Rule::Modified50), Some(0.50));
147        assert_eq!(bar_threshold(Rule::Contributory), Some(0.0));
148    }
149}