use crate::error::{Result, SanghaError};
#[must_use = "returns the Gini coefficient without side effects"]
pub fn gini_coefficient(incomes: &[f64]) -> Result<f64> {
if incomes.is_empty() {
return Err(SanghaError::ComputationError(
"need at least one income value".into(),
));
}
if incomes.iter().any(|&v| v < 0.0 || !v.is_finite()) {
return Err(SanghaError::ComputationError(
"all income values must be finite and non-negative".into(),
));
}
let n = incomes.len();
if n == 1 {
return Ok(0.0);
}
let mut sorted: Vec<f64> = incomes.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let total: f64 = sorted.iter().sum();
if total.abs() < f64::EPSILON {
return Ok(0.0);
}
let mut sum_weighted = 0.0;
for (i, &val) in sorted.iter().enumerate() {
sum_weighted += (i as f64 + 1.0) * val;
}
let n_f = n as f64;
let gini = (2.0 * sum_weighted) / (n_f * total) - (n_f + 1.0) / n_f;
Ok(gini)
}
#[must_use = "returns the Lorenz curve points without side effects"]
pub fn lorenz_curve(incomes: &[f64]) -> Result<Vec<(f64, f64)>> {
if incomes.is_empty() {
return Err(SanghaError::ComputationError(
"need at least one income value".into(),
));
}
if incomes.iter().any(|&v| v < 0.0 || !v.is_finite()) {
return Err(SanghaError::ComputationError(
"all income values must be finite and non-negative".into(),
));
}
let mut sorted: Vec<f64> = incomes.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let total: f64 = sorted.iter().sum();
let n = sorted.len() as f64;
let mut points = Vec::with_capacity(sorted.len() + 1);
points.push((0.0, 0.0));
let mut cumulative = 0.0;
for (i, &val) in sorted.iter().enumerate() {
cumulative += val;
let pop_frac = (i as f64 + 1.0) / n;
let income_frac = if total > 0.0 {
cumulative / total
} else {
pop_frac
};
points.push((pop_frac, income_frac));
}
Ok(points)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gini_perfect_equality() {
let incomes = vec![100.0, 100.0, 100.0, 100.0, 100.0];
let g = gini_coefficient(&incomes).unwrap();
assert!(g.abs() < 1e-10);
}
#[test]
fn test_gini_high_inequality() {
let incomes = vec![0.0, 0.0, 0.0, 0.0, 1000.0];
let g = gini_coefficient(&incomes).unwrap();
assert!(g > 0.7); }
#[test]
fn test_gini_moderate() {
let incomes = vec![10.0, 20.0, 30.0, 40.0, 50.0];
let g = gini_coefficient(&incomes).unwrap();
assert!(g > 0.0);
assert!(g < 0.5);
}
#[test]
fn test_gini_empty() {
assert!(gini_coefficient(&[]).is_err());
}
#[test]
fn test_gini_single() {
let g = gini_coefficient(&[100.0]).unwrap();
assert!((g - 0.0).abs() < 1e-10);
}
#[test]
fn test_lorenz_curve_shape() {
let incomes = vec![10.0, 20.0, 30.0, 40.0];
let curve = lorenz_curve(&incomes).unwrap();
assert_eq!(curve.len(), 5); assert_eq!(curve[0], (0.0, 0.0));
let last = curve.last().unwrap();
assert!((last.0 - 1.0).abs() < 1e-10);
assert!((last.1 - 1.0).abs() < 1e-10);
}
#[test]
fn test_lorenz_curve_equality() {
let incomes = vec![25.0, 25.0, 25.0, 25.0];
let curve = lorenz_curve(&incomes).unwrap();
for &(pop, income) in &curve {
assert!((pop - income).abs() < 1e-10);
}
}
#[test]
fn test_gini_negative_income_error() {
assert!(gini_coefficient(&[10.0, -5.0, 20.0]).is_err());
}
#[test]
fn test_gini_nan_error() {
assert!(gini_coefficient(&[10.0, f64::NAN]).is_err());
}
#[test]
fn test_gini_all_zeros() {
let g = gini_coefficient(&[0.0, 0.0, 0.0]).unwrap();
assert!((g - 0.0).abs() < 1e-10);
}
#[test]
fn test_lorenz_curve_empty_error() {
assert!(lorenz_curve(&[]).is_err());
}
#[test]
fn test_lorenz_curve_negative_error() {
assert!(lorenz_curve(&[10.0, -5.0]).is_err());
}
#[test]
fn test_lorenz_curve_single() {
let curve = lorenz_curve(&[100.0]).unwrap();
assert_eq!(curve.len(), 2);
assert_eq!(curve[0], (0.0, 0.0));
assert!((curve[1].0 - 1.0).abs() < 1e-10);
assert!((curve[1].1 - 1.0).abs() < 1e-10);
}
}