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}