Skip to main content

use_ion/
lib.rs

1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5//! Ion identity and charge primitives.
6
7mod anion;
8mod cation;
9mod charge_sign;
10mod error;
11mod ion;
12mod ion_charge;
13mod ion_formula;
14mod ion_kind;
15mod ion_name;
16mod monatomic_ion;
17mod polyatomic_ion;
18
19pub use anion::Anion;
20pub use cation::Cation;
21pub use charge_sign::ChargeSign;
22pub use error::IonValidationError;
23pub use ion::Ion;
24pub use ion_charge::{ChargeMagnitude, IonCharge};
25pub use ion_formula::IonFormula;
26pub use ion_kind::IonKind;
27pub use ion_name::IonName;
28pub use monatomic_ion::MonatomicIon;
29pub use polyatomic_ion::PolyatomicIon;
30
31#[cfg(test)]
32mod tests {
33    use use_chemical_formula::ChemicalFormula;
34
35    use super::{
36        Anion, Cation, ChargeMagnitude, ChargeSign, Ion, IonCharge, IonFormula, IonKind, IonName,
37        IonValidationError, MonatomicIon, PolyatomicIon,
38    };
39
40    fn formula(input: &str) -> ChemicalFormula {
41        ChemicalFormula::parse(input).expect("test formula should parse")
42    }
43
44    fn positive(magnitude: u8) -> IonCharge {
45        IonCharge::positive(magnitude).expect("positive charge should be valid")
46    }
47
48    fn negative(magnitude: u8) -> IonCharge {
49        IonCharge::negative(magnitude).expect("negative charge should be valid")
50    }
51
52    #[test]
53    fn creates_positive_monatomic_ions() {
54        let sodium = Ion::new(formula("Na"), positive(1)).with_kind(IonKind::Monatomic);
55        let calcium = Ion::new(formula("Ca"), positive(2));
56
57        assert!(sodium.is_cation());
58        assert_eq!(sodium.charge().magnitude(), 1);
59        assert_eq!(sodium.to_string(), "Na+");
60        assert_eq!(calcium.to_string(), "Ca^2+");
61    }
62
63    #[test]
64    fn creates_negative_monatomic_ions() {
65        let chloride = Ion::new(formula("Cl"), negative(1)).with_kind(IonKind::Monatomic);
66
67        assert!(chloride.is_anion());
68        assert_eq!(chloride.charge().sign(), ChargeSign::Negative);
69        assert_eq!(chloride.to_string(), "Cl-");
70    }
71
72    #[test]
73    fn creates_positive_polyatomic_ions() {
74        let ammonium = Ion::new(formula("NH4"), positive(1)).with_kind(IonKind::Polyatomic);
75
76        assert!(ammonium.is_cation());
77        assert_eq!(ammonium.kinds(), &[IonKind::Polyatomic]);
78        assert_eq!(ammonium.to_string(), "NH4+");
79    }
80
81    #[test]
82    fn creates_negative_polyatomic_ions() {
83        let sulfate = Ion::new(formula("SO4"), negative(2)).with_kind(IonKind::Polyatomic);
84        let carbonate = Ion::new(formula("CO3"), negative(2));
85
86        assert!(sulfate.is_anion());
87        assert_eq!(sulfate.to_string(), "SO4^2-");
88        assert_eq!(carbonate.to_string(), "CO3^2-");
89    }
90
91    #[test]
92    fn validates_charge_magnitude() {
93        assert_eq!(
94            ChargeMagnitude::new(0),
95            Err(IonValidationError::ZeroChargeMagnitude)
96        );
97        assert_eq!(
98            IonCharge::positive(0),
99            Err(IonValidationError::ZeroChargeMagnitude)
100        );
101        assert_eq!(
102            IonCharge::negative(0),
103            Err(IonValidationError::ZeroChargeMagnitude)
104        );
105    }
106
107    #[test]
108    fn exposes_charge_sign_helpers() {
109        assert!(ChargeSign::Positive.is_positive());
110        assert!(ChargeSign::Negative.is_negative());
111        assert_eq!(ChargeSign::Positive.to_string(), "+");
112        assert_eq!(ChargeSign::Negative.to_string(), "-");
113        assert_eq!(positive(3).to_string(), "3+");
114        assert_eq!(negative(2).to_string(), "2-");
115    }
116
117    #[test]
118    fn detects_cations_and_anions_from_charge() {
119        let sodium = Ion::new(formula("Na"), positive(1));
120        let chloride = Ion::new(formula("Cl"), negative(1));
121
122        assert!(sodium.is_cation());
123        assert!(!sodium.is_anion());
124        assert!(chloride.is_anion());
125        assert!(!chloride.is_cation());
126    }
127
128    #[test]
129    fn exposes_formula_wrappers() {
130        let formula = IonFormula::new(formula("NO3"));
131        let nitrate = Ion::new(formula.clone().into_formula(), negative(1));
132
133        assert_eq!(nitrate.formula().to_string(), "NO3");
134        assert_eq!(nitrate.ion_formula().to_string(), "NO3");
135        assert_eq!(nitrate.to_string(), "NO3-");
136    }
137
138    #[test]
139    fn validates_ion_names() {
140        let hydronium = Ion::new(formula("H3O"), positive(1))
141            .try_with_name(" hydronium ")
142            .expect("ion name should be valid");
143
144        assert_eq!(IonName::new("  "), Err(IonValidationError::EmptyName));
145        assert_eq!(hydronium.name().map(IonName::as_str), Some("hydronium"));
146        assert_eq!(hydronium.to_string(), "H3O+");
147    }
148
149    #[test]
150    fn rejects_invalid_empty_names() {
151        assert_eq!(
152            Ion::new(formula("Na"), positive(1)).try_with_name(""),
153            Err(IonValidationError::EmptyName)
154        );
155    }
156
157    #[test]
158    fn wraps_cations_and_anions() {
159        let calcium = Cation::new(formula("Ca"), 2).expect("cation should be valid");
160        let chloride = Anion::new(formula("Cl"), 1).expect("anion should be valid");
161
162        assert_eq!(calcium.as_ion().to_string(), "Ca^2+");
163        assert_eq!(chloride.as_ion().to_string(), "Cl-");
164        assert_eq!(
165            Cation::from_ion(chloride.as_ion().clone()),
166            Err(IonValidationError::ExpectedCation)
167        );
168        assert_eq!(
169            Anion::from_ion(calcium.as_ion().clone()),
170            Err(IonValidationError::ExpectedAnion)
171        );
172    }
173
174    #[test]
175    fn wraps_monatomic_and_polyatomic_ions() {
176        let sodium =
177            MonatomicIon::new(formula("Na"), positive(1)).expect("monatomic ion should be valid");
178        let ammonium = PolyatomicIon::new(formula("NH4"), positive(1))
179            .expect("polyatomic ion should be valid");
180
181        assert_eq!(sodium.as_ion().kinds(), &[IonKind::Monatomic]);
182        assert_eq!(ammonium.as_ion().kinds(), &[IonKind::Polyatomic]);
183        assert_eq!(
184            MonatomicIon::new(formula("NH4"), positive(1)),
185            Err(IonValidationError::ExpectedMonatomicFormula)
186        );
187        assert_eq!(
188            PolyatomicIon::new(formula("Na"), positive(1)),
189            Err(IonValidationError::ExpectedPolyatomicFormula)
190        );
191    }
192
193    #[test]
194    fn assigns_oxidation_state_labels() {
195        let iron = Ion::new(formula("Fe"), positive(3))
196            .try_with_oxidation_state_label("III")
197            .expect("oxidation-state label should be valid");
198
199        assert_eq!(iron.oxidation_state_label(), Some("III"));
200        assert_eq!(iron.to_string(), "Fe^3+");
201        assert_eq!(
202            Ion::new(formula("Fe"), positive(2)).try_with_oxidation_state_label(" "),
203            Err(IonValidationError::EmptyOxidationStateLabel)
204        );
205    }
206}