use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChargeBalance {
pub sum_cations_meq: f64,
pub sum_anions_meq: f64,
pub balance_percent: f64,
}
fn ion_properties(name: &str) -> Option<(f64, f64)> {
match name.to_uppercase().as_str() {
"NA" | "NA+" => Some((22.990, 1.0)),
"K" | "K+" => Some((39.098, 1.0)),
"MG" | "MG2+" => Some((24.305, 2.0)),
"CA" | "CA2+" => Some((40.078, 2.0)),
"NH4" | "NH4+" => Some((18.039, 1.0)),
"H" | "H+" => Some((1.008, 1.0)),
"FE2+" => Some((55.845, 2.0)),
"MN2+" => Some((54.938, 2.0)),
"CL" | "CL-" => Some((35.453, 1.0)),
"SO4" | "SO42-" => Some((96.066, 2.0)),
"NO3" | "NO3-" => Some((62.004, 1.0)),
"HCO3" | "HCO3-" => Some((61.017, 1.0)),
"F" | "F-" => Some((18.998, 1.0)),
"NO2" | "NO2-" => Some((46.006, 1.0)),
"PO4" | "PO43-" => Some((94.971, 3.0)),
_ => None,
}
}
#[must_use]
pub fn charge_balance(cations: &[(&str, f64)], anions: &[(&str, f64)]) -> ChargeBalance {
let to_meq = |ions: &[(&str, f64)]| -> f64 {
ions.iter()
.filter_map(|(name, conc_mg_l)| {
ion_properties(name).map(|(molar_mass, charge)| conc_mg_l / molar_mass * charge)
})
.sum()
};
let sum_cations_meq = to_meq(cations);
let sum_anions_meq = to_meq(anions);
let total = sum_cations_meq + sum_anions_meq;
let balance_percent = if total == 0.0 {
0.0
} else {
(sum_cations_meq - sum_anions_meq) / total * 100.0
};
ChargeBalance {
sum_cations_meq,
sum_anions_meq,
balance_percent,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_charge_balance_equal() {
let result = charge_balance(&[("Na", 22.990)], &[("Cl", 35.453)]);
assert!(
result.balance_percent.abs() < 0.1,
"expected ~0%, got {:.4}%",
result.balance_percent
);
}
#[test]
fn test_charge_balance_imbalanced() {
let result = charge_balance(&[("Na", 45.98)], &[("Cl", 35.453)]);
assert!(
result.balance_percent > 0.0,
"expected positive imbalance, got {:.4}%",
result.balance_percent
);
}
#[test]
fn test_charge_balance_empty() {
let result = charge_balance(&[], &[]);
assert!(
(result.balance_percent - 0.0).abs() < f64::EPSILON,
"expected 0%, got {:.4}%",
result.balance_percent
);
}
#[test]
fn test_divalent_ions() {
let result = charge_balance(&[("Ca", 40.078)], &[("SO4", 96.066)]);
assert!(
result.balance_percent.abs() < 0.1,
"expected ~0%, got {:.4}%",
result.balance_percent
);
}
}