use core::fmt::{Display, Formatter};
use num_integer::gcd;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub enum Salt {
#[cfg_attr(feature = "serde", serde(rename = "NaCl"))]
#[default]
SodiumChloride,
#[cfg_attr(feature = "serde", serde(rename = "CaCl₂"))]
CalciumChloride,
#[cfg_attr(feature = "serde", serde(rename = "CaSO₄"))]
CalciumSulfate,
#[cfg_attr(feature = "serde", serde(rename = "KAl(SO₄)₂"))]
PotassiumAlum,
#[cfg_attr(feature = "serde", serde(rename = "Na₂SO₄"))]
SodiumSulfate,
#[cfg_attr(feature = "serde", serde(rename = "LaCl₃"))]
LanthanumChloride,
Custom(Vec<isize>),
}
impl Salt {
pub fn valencies(&self) -> Vec<isize> {
match self {
Salt::SodiumChloride => vec![1, -1],
Salt::CalciumChloride => vec![2, -1],
Salt::CalciumSulfate => vec![2, -2],
Salt::PotassiumAlum => vec![1, 3, -2],
Salt::SodiumSulfate => vec![1, -2],
Salt::LanthanumChloride => vec![3, -1],
Salt::Custom(valencies) => valencies.clone(),
}
}
pub fn stoichiometry(&self) -> crate::Result<Vec<usize>> {
let valencies = self.valencies();
let sum_positive: isize = valencies.iter().filter(|i| i.is_positive()).sum();
let sum_negative: isize = valencies.iter().filter(|i| i.is_negative()).sum();
let gcd = gcd(sum_positive, sum_negative);
if sum_positive == 0 || sum_negative == 0 || gcd == 0 {
return Err(crate::Error::Stoichiometry);
}
Ok(valencies
.iter()
.map(|valency| {
((match valency.is_positive() {
true => -sum_negative,
false => sum_positive,
}) / gcd) as usize
})
.collect())
}
pub fn ionic_strength(&self, molarity: f64) -> crate::Result<f64> {
Ok(0.5
* molarity
* std::iter::zip(self.valencies(), self.stoichiometry()?.iter().copied())
.map(|(valency, nu)| nu * valency.pow(2) as usize)
.sum::<usize>() as f64)
}
}
impl Display for Salt {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "🧂Salt = ")?;
match self {
Salt::SodiumChloride => write!(f, "NaCl"),
Salt::CalciumChloride => write!(f, "CaCl₂"),
Salt::CalciumSulfate => write!(f, "CaSO₄"),
Salt::PotassiumAlum => write!(f, "KAl(SO₄)₂"),
Salt::SodiumSulfate => write!(f, "Na₂SO₄"),
Salt::LanthanumChloride => write!(f, "LaCl₃"),
Salt::Custom(valencies) => {
write!(f, "Custom(")?;
for (i, valency) in valencies.iter().enumerate() {
write!(f, "{}", valency)?;
if i < valencies.len() - 1 {
write!(f, ", ")?;
}
}
write!(f, ")")
}
}
}
}
#[test]
fn test_salt() {
let molarity = 0.15;
assert_eq!(Salt::SodiumChloride.valencies(), [1, -1]);
assert_eq!(Salt::SodiumChloride.stoichiometry().unwrap(), [1, 1]);
approx::assert_abs_diff_eq!(
Salt::SodiumChloride.ionic_strength(molarity).unwrap(),
molarity
);
assert_eq!(Salt::CalciumSulfate.valencies(), [2, -2]);
assert_eq!(Salt::CalciumSulfate.stoichiometry().unwrap(), [1, 1]);
approx::assert_abs_diff_eq!(
Salt::CalciumSulfate.ionic_strength(molarity).unwrap(),
0.5 * (molarity * 4.0 + molarity * 4.0)
);
assert_eq!(Salt::CalciumChloride.valencies(), [2, -1]);
assert_eq!(Salt::CalciumChloride.stoichiometry().unwrap(), [1, 2]);
approx::assert_abs_diff_eq!(
Salt::CalciumChloride.ionic_strength(molarity).unwrap(),
0.5 * (molarity * 4.0 + 2.0 * molarity)
);
assert_eq!(Salt::PotassiumAlum.valencies(), [1, 3, -2]);
assert_eq!(Salt::PotassiumAlum.stoichiometry().unwrap(), [1, 1, 2]);
approx::assert_abs_diff_eq!(
Salt::PotassiumAlum.ionic_strength(molarity).unwrap(),
0.5 * (molarity * 1.0 + molarity * 9.0 + 2.0 * molarity * 4.0)
);
let invalid_salt = Salt::Custom(vec![1, 1]); assert!(invalid_salt.stoichiometry().is_err());
assert!(invalid_salt.ionic_strength(0.1).is_err());
}