chem_eq/
equation.rs

1use std::str::FromStr;
2
3use itertools::Itertools;
4
5use crate::{
6    compound::Compound,
7    error::{ConcentrationError, ConcentrationNameError, EquationError},
8    parse, Direction, State,
9};
10
11// Used for rustdoc
12#[cfg(all(doc, feature = "balance"))]
13#[allow(unused)]
14use crate::balance::EquationBalancer;
15
16/// A Chemical Equation. Containing a left and right side. Also keeps
17/// track of the mol ratio.
18///
19/// Eg: `4Fe + 3O2 -> 2Fe2O3`
20#[derive(Debug, Default, Clone, PartialOrd)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct Equation {
23    pub(crate) left: Vec<Compound>,
24    pub(crate) right: Vec<Compound>,
25    pub(crate) direction: Direction,
26    pub(crate) equation: String,
27    pub(crate) delta_h: f32,
28    pub(crate) temperature: Option<f32>,
29    pub(crate) volume: Option<f32>,
30}
31
32impl PartialEq for Equation {
33    fn eq(&self, other: &Self) -> bool {
34        self.left() == other.left()
35            && self.right() == other.right()
36            && self.direction() == other.direction()
37            && self.equation() == other.equation()
38            && self.delta_h() == other.delta_h()
39            && self.temperature() == other.temperature()
40            && self.volume() == other.volume()
41    }
42}
43impl Eq for Equation {}
44
45impl TryFrom<&str> for Equation {
46    type Error = EquationError;
47
48    fn try_from(s: &str) -> Result<Self, EquationError> {
49        Self::new(s)
50    }
51}
52
53impl FromStr for Equation {
54    type Err = EquationError;
55
56    fn from_str(s: &str) -> Result<Self, Self::Err> {
57        Self::new(s)
58    }
59}
60
61impl Equation {
62    /// Create an [`Equation`] from a [`str`]. Fails if the string couldn't
63    /// be parsed.
64    ///
65    /// # Examples
66    ///
67    /// ```rust
68    /// use chem_eq::{Equation, error::EquationError};
69    ///
70    /// let eq = Equation::new("2H2 + O2 -> 2H2O");
71    /// assert!(eq.is_ok());
72    ///
73    /// let eq = Equation::new("H2b + bad_name == joe");
74    /// assert!(matches!(eq, Err(EquationError::ParsingError(_))));
75    /// ```
76    pub fn new(input: &str) -> Result<Self, EquationError> {
77        match parse::parse_equation(input) {
78            Ok((i, _)) if !i.trim().is_empty() => Err(EquationError::TooMuchInput(i.to_string())),
79            Ok((_, eq)) if eq.is_valid() => Ok(eq),
80            Ok(_) => Err(EquationError::IncorrectEquation),
81            Err(nom::Err::Error(e) | nom::Err::Failure(e)) => {
82                Err(EquationError::ParsingError(e.into()))
83            }
84            // no streaming parsers were used
85            Err(nom::Err::Incomplete(_)) => unreachable!(),
86        }
87    }
88
89    /// Get the mol ratio of the equation (left over right). Will count any compound
90    /// with no specified state.
91    ///
92    /// # Examples
93    ///
94    /// ```rust
95    /// use chem_eq::Equation;
96    ///
97    /// // returns left over right
98    /// // if states aren't given, everything is counted
99    /// let eq = Equation::new("2H2 + O2 -> 2H2O").unwrap();
100    /// assert_eq!(eq.mol_ratio(), (3, 2));
101    ///
102    /// // doesn't matter how bad an equation this is...
103    /// let eq = Equation::new("4FeH3(s) + 3O2(g) -> 2Fe2O3(s) + 6H2(g)").unwrap();
104    /// assert_eq!(eq.mol_ratio(), (3, 6));
105    /// ```
106    pub fn mol_ratio(&self) -> (usize, usize) {
107        let left = self
108            .left
109            .iter()
110            .filter(|c| {
111                c.state
112                    .as_ref()
113                    .map_or(true, |s| matches!(s, State::Aqueous | State::Gas))
114            })
115            .map(|c| c.coefficient)
116            .sum::<usize>();
117        let right = self
118            .right
119            .iter()
120            .filter(|c| {
121                c.state
122                    .as_ref()
123                    .map_or(true, |s| matches!(s, State::Aqueous | State::Gas))
124            })
125            .map(|c| c.coefficient)
126            .sum::<usize>();
127        if left == 0 && right == 0 {
128            (1, 1)
129        } else {
130            (left, right)
131        }
132    }
133
134    /// Get a vec of each unique element name
135    ///
136    /// # Examples
137    ///
138    /// ```rust
139    /// use chem_eq::Equation;
140    ///
141    /// let eq = Equation::new("2O2 + H2 -> 2H2O").unwrap();
142    /// assert_eq!(eq.uniq_elements().len(), 2);
143    ///
144    /// let eq =
145    ///     Equation::new("3(NH4)2SO4(aq) + Fe3(PO4)2(s) <- 2(NH4)3PO4(aq) + 3FeSO4(aq)").unwrap();
146    /// assert_eq!(eq.uniq_elements().len(), 6);
147    /// ```
148    pub fn uniq_elements(&self) -> Vec<&str> {
149        // get the name of every element in the equation
150        let element_names = self
151            .iter_compounds()
152            .flat_map(|c| &c.elements)
153            .map(|e| e.symbol())
154            .unique()
155            .collect::<Vec<&str>>();
156
157        element_names
158    }
159
160    /// Count how many compounds are in the whole equation.
161    ///
162    /// # Examples
163    ///
164    /// ```rust
165    /// use chem_eq::Equation;
166    ///
167    /// let eq = Equation::new("O2 + 2H2 -> 2H2O").unwrap();
168    /// assert_eq!(eq.num_compounds(), 3);
169    ///
170    /// let eq = Equation::new("3(NH4)2SO4(aq) + Fe3(PO4)2(s) <- 2(NH4)3PO4(aq) + 3FeSO4(aq)").unwrap();
171    /// assert_eq!(eq.num_compounds(), 4);
172    /// ```
173    pub fn num_compounds(&self) -> usize {
174        self.left.len() + self.right.len()
175    }
176
177    /// Check if an equation is valid.
178    ///
179    /// # Examples
180    ///
181    /// ```rust
182    /// use chem_eq::{Equation, error::EquationError};
183    ///
184    /// let eq = Equation::new("O2 + 2H2 -> 2H2O");
185    /// assert!(eq.is_ok());
186    ///
187    /// let eq = Equation::new("Fe + S8 -> Fe2O3");
188    /// // fails because the equation doesn't have sulfur and oxygen on both sides
189    /// assert_eq!(eq, Err(EquationError::IncorrectEquation));
190    /// ```
191    pub(crate) fn is_valid(&self) -> bool {
192        let mut left_elements = self
193            .left
194            .iter()
195            .flat_map(|c| &c.elements)
196            .map(|e| e.symbol())
197            .unique()
198            .collect::<Vec<&str>>();
199        let mut right_elements = self
200            .right
201            .iter()
202            .flat_map(|c| &c.elements)
203            .map(|e| e.symbol())
204            .unique()
205            .collect::<Vec<&str>>();
206
207        // sort to make sure comparisons work
208        left_elements.sort_unstable();
209        right_elements.sort_unstable();
210
211        // simple verification that the same elements are on both sides
212        left_elements == right_elements
213    }
214
215    /// Reconstruct original equation without using the saved original string.
216    ///
217    /// # Examples
218    ///
219    /// ```rust
220    /// use chem_eq::Equation;
221    ///
222    /// let eq = Equation::new("O2 + H2 -> H2O").unwrap();
223    /// assert_eq!(eq.reconstruct(), "1O2 + 1H2 -> 1H2O1");
224    /// ```
225    pub fn reconstruct(&self) -> String {
226        format!(
227            "{} {} {}",
228            self.left
229                .iter()
230                .map(ToString::to_string)
231                .collect::<Vec<String>>()
232                .join(" + "),
233            self.direction,
234            self.right
235                .iter()
236                .map(ToString::to_string)
237                .collect::<Vec<String>>()
238                .join(" + "),
239        )
240    }
241
242    /// Create an iterator over all compounds of an equation
243    ///
244    /// # Examples
245    ///
246    /// ```rust
247    /// use chem_eq::{Equation, Compound};
248    ///
249    /// let eq = Equation::new("O2 + H2 -> H2O").unwrap();
250    /// assert_eq!(eq.iter_compounds().collect::<Vec<&Compound>>().len(), 3);
251    /// ```
252    // Mostly as a convenience method as this appears in multiple places
253    pub fn iter_compounds(&self) -> impl Iterator<Item = &Compound> {
254        self.left.iter().chain(self.right.iter())
255    }
256
257    /// Create a mutable iterator over all compounds of an equation.
258    ///
259    /// # Examples
260    ///
261    /// ```rust
262    /// use chem_eq::{Equation, Compound};
263    ///
264    /// let mut eq = Equation::new("O2 + H2 -> H2O").unwrap();
265    /// assert_eq!(eq.iter_compounds_mut().collect::<Vec<&mut Compound>>().len(), 3);
266    /// ```
267    // Mostly as a convenience method as this appears in multiple places
268    pub fn iter_compounds_mut(&mut self) -> impl Iterator<Item = &mut Compound> {
269        self.left.iter_mut().chain(self.right.iter_mut())
270    }
271
272    /// Check if the equation is balanced
273    ///
274    /// # Examples
275    ///
276    /// ```rust
277    /// use chem_eq::Equation;
278    ///
279    /// let eq = Equation::new("C + 2H2O -> CO2 + 2H2").unwrap();
280    /// assert!(eq.is_balanced());
281    ///
282    /// let eq = Equation::new("Mg(OH)2 + Fe -> Fe(OH)3 + Mg").unwrap();
283    /// assert!(!eq.is_balanced());
284    /// ```
285    #[cfg(feature = "balance")]
286    #[cfg_attr(docsrs, doc(cfg(feature = "balance")))]
287    pub fn is_balanced(&self) -> bool {
288        use std::collections::HashMap;
289        let mut lhs: HashMap<&str, usize> = HashMap::default();
290        let mut rhs: HashMap<&str, usize> = HashMap::default();
291
292        // left hand side
293        for cmp in &self.left {
294            for el in &cmp.elements {
295                let count = lhs.get(el.symbol()).unwrap_or(&0);
296                lhs.insert(el.symbol(), count + el.count * cmp.coefficient);
297            }
298        }
299
300        // right hand side
301        for cmp in &self.right {
302            for el in &cmp.elements {
303                let count = rhs.get(el.symbol()).unwrap_or(&0);
304                rhs.insert(el.symbol(), count + el.count * cmp.coefficient);
305            }
306        }
307
308        // different amount of elements
309        lhs.len() == rhs.len()
310            && lhs.keys().all(|k| {
311                if rhs.contains_key(k) {
312                    return lhs.get(k).unwrap() == rhs.get(k).unwrap();
313                }
314                false
315            })
316    }
317
318    /// Create an [`EquationBalancer`], mostly as a convenience method.
319    ///
320    /// ## Examples
321    ///
322    /// ```rust
323    /// use chem_eq::Equation;
324    ///
325    /// let eq = Equation::new("H2 + O2 -> H2O").unwrap().to_balancer().balance().unwrap();
326    /// assert_eq!(eq.equation(), "2H2 + O2 -> 2H2O".to_string());
327    /// ```
328    #[cfg(feature = "balance")]
329    #[cfg_attr(docsrs, doc(cfg(feature = "balance")))]
330    pub fn to_balancer(&self) -> crate::balance::EquationBalancer {
331        use crate::balance::EquationBalancer;
332
333        EquationBalancer::new(self)
334    }
335
336    /// Check whether an equation is exothermic
337    ///
338    /// # Examples
339    ///
340    /// ```rust
341    /// use chem_eq::Equation;
342    ///
343    /// let mut eq = Equation::new("2Mg(s) + O2(g) -> 2MgO(s)").unwrap();
344    /// eq.set_delta_h(-601.1);
345    /// assert!(eq.is_exothermic());
346    /// ```
347    pub fn is_exothermic(&self) -> bool {
348        self.delta_h() < 0.0
349    }
350
351    /// Check whether an equation is endothermic
352    ///
353    /// # Examples
354    ///
355    /// ```rust
356    /// use chem_eq::Equation;
357    ///
358    /// let mut eq = Equation::new("6CO2 + 6H2O -> C6H12O6 + 6O2").unwrap();
359    /// eq.set_delta_h(2802.7);
360    /// assert!(eq.is_endothermic());
361    /// ```
362    pub fn is_endothermic(&self) -> bool {
363        self.delta_h() > 0.0
364    }
365
366    /// Get an iterator over each compounds name.
367    ///
368    /// # Examples
369    ///
370    /// ```rust
371    /// use chem_eq::Equation;
372    ///
373    /// let eq = Equation::new("H2 + O2 -> H2O").unwrap();
374    /// assert_eq!(vec!["H2", "O2", "H2O"], eq.compound_names().collect::<Vec<&str>>());
375    ///
376    /// let eq = Equation::new("Fe2O3 <- Fe + O2").unwrap();
377    /// assert_eq!(vec!["Fe2O3", "Fe", "O2"], eq.compound_names().collect::<Vec<&str>>());
378    /// ```
379    pub fn compound_names(&self) -> impl Iterator<Item = &str> {
380        self.equation()
381            .split(' ')
382            .filter(|s| !matches!(*s, "+" | "<-" | "<->" | "->"))
383    }
384
385    /// Get an iterator for each concentration in an equation
386    pub fn concentrations(&self) -> impl Iterator<Item = &f32> {
387        self.iter_compounds().map(|cmp| &cmp.concentration)
388    }
389
390    /// Get a mutable iterator for each concentration in an equation
391    pub fn concentrations_mut(&mut self) -> impl Iterator<Item = &mut f32> {
392        self.iter_compounds_mut().map(|cmp| &mut cmp.concentration)
393    }
394
395    /// Get an iterator yielding compound names and concentrations
396    pub fn name_and_concentration(&self) -> impl Iterator<Item = (&str, &f32)> {
397        self.compound_names().zip(self.concentrations())
398    }
399
400    /// Get a mutable iterator yielding compound names and mutable concentrations
401    pub fn name_and_concentration_mut(&mut self) -> impl Iterator<Item = (String, &mut f32)> {
402        self.equation
403            .split(' ')
404            .filter(|s| !matches!(*s, "+" | "<-" | "<->" | "->"))
405            .map(ToString::to_string)
406            .collect_vec()
407            .into_iter()
408            .zip(self.concentrations_mut())
409    }
410
411    /// Get a vec of all concentrations
412    ///
413    /// # Examples
414    ///
415    /// ```rust
416    /// use chem_eq::Equation;
417    ///
418    /// let mut eq = Equation::new("H2 + O2 -> H2O").unwrap();
419    /// assert_eq!(eq.get_concentrations(), vec![0.0, 0.0, 0.0]);
420    ///
421    /// eq.set_concentrations(&[1.0, 2.0, 3.0]);
422    /// assert_eq!(eq.get_concentrations(), vec![1.0, 2.0, 3.0]);
423    /// ```
424    pub fn get_concentrations(&self) -> Vec<f32> {
425        self.concentrations().copied().collect()
426    }
427
428    /// Set concentrations with a slice. A convenience method to quickly set all
429    /// compounds to have a concentration.
430    ///
431    /// # Examples
432    ///
433    /// ```rust
434    /// use chem_eq::{Equation, error::ConcentrationError};
435    ///
436    /// let mut eq = Equation::new("H2 + O2 -> H2O").unwrap();
437    /// eq.set_concentrations(&[1.0, 2.0, 3.0]).unwrap();
438    /// assert_eq!(eq.get_concentrations(), vec![1.0, 2.0, 3.0]);
439    ///
440    /// assert_eq!(eq.set_concentrations(&[1.0, 34.0]), Err(ConcentrationError::WrongSliceSize));
441    /// ```
442    pub fn set_concentrations(&mut self, concentrations: &[f32]) -> Result<(), ConcentrationError> {
443        // check assumptions
444        if concentrations.len() != self.num_compounds() {
445            return Err(ConcentrationError::WrongSliceSize);
446        }
447        if concentrations.iter().any(|&c| c.is_nan()) {
448            return Err(ConcentrationError::NAN);
449        }
450
451        for (orig, new) in self.concentrations_mut().zip(concentrations.iter()) {
452            *orig = *new;
453        }
454
455        Ok(())
456    }
457
458    /// Get a singular compounds concentration by its name.
459    ///
460    /// # Examples
461    ///
462    /// ```rust
463    /// use chem_eq::{Equation, error::ConcentrationNameError};
464    ///
465    /// let mut eq = Equation::new("H2 + O2 -> H2O").unwrap();
466    /// eq.set_concentration_by_name("O2", 0.25).unwrap();
467    /// assert_eq!(eq.get_concentration_by_name("O2"), Ok(0.25));
468    ///
469    /// assert_eq!(eq.get_concentration_by_name("joe"), Err(ConcentrationNameError::NotFound));
470    /// ```
471    pub fn get_concentration_by_name(&self, name: &str) -> Result<f32, ConcentrationNameError> {
472        // I don't like the collecting here...
473        // but I can't avoid double borrowing self as mutable and immutable
474        let (_name, cmp) = self
475            .compound_names()
476            .map(ToString::to_string)
477            .collect_vec()
478            .into_iter()
479            .zip(self.iter_compounds())
480            .find(|(n, _c)| *n == name)
481            .ok_or(ConcentrationNameError::NotFound)?;
482        Ok(cmp.concentration)
483    }
484
485    /// Get a compound by name
486    ///
487    /// # Examples
488    ///
489    /// ```rust
490    /// use chem_eq::{Equation, Compound};
491    ///
492    /// let mut eq = Equation::new("H2 + O2 -> H2O").unwrap();
493    /// let cmp = eq.get_compound_by_name("O2");
494    /// assert_eq!(cmp.unwrap(), &Compound::parse("O2").unwrap());
495    ///
496    /// assert!(eq.get_compound_by_name("joe").is_none());
497    /// ```
498    pub fn get_compound_by_name(&self, name: &str) -> Option<&Compound> {
499        self.compound_names()
500            .zip(self.iter_compounds())
501            .find(|(n, _c)| *n == name)
502            .map(|(_n, c)| c)
503    }
504
505    /// Get a mutable reference to a compound by name
506    ///
507    /// # Examples
508    ///
509    /// ```rust
510    /// use chem_eq::{Equation, Compound};
511    ///
512    /// let mut eq = Equation::new("H2 + O2 -> H2O").unwrap();
513    /// let cmp = eq.get_compound_by_name_mut("O2");
514    /// assert_eq!(cmp.unwrap(), &mut Compound::parse("O2").unwrap());
515    ///
516    /// assert!(eq.get_compound_by_name("joe").is_none());
517    /// ```
518    pub fn get_compound_by_name_mut(&mut self, name: &str) -> Option<&mut Compound> {
519        let i = self
520            .compound_names()
521            .enumerate()
522            .find(|(_i, n)| *n == name)
523            .map(|(i, _n)| i)?;
524        self.nth_compound_mut(i)
525    }
526
527    /// Set a singular compounds concentration by its name.
528    ///
529    /// # Examples
530    ///
531    /// ```rust
532    /// use chem_eq::{Equation, error::ConcentrationNameError};
533    ///
534    /// let mut eq = Equation::new("H2 + O2 -> H2O").unwrap();
535    /// eq.set_concentration_by_name("O2", 0.25).unwrap();
536    /// assert_eq!(eq.get_concentrations(), vec![0.0, 0.25, 0.0]);
537    ///
538    /// assert_eq!(eq.set_concentration_by_name("joe", 24.0), Err(ConcentrationNameError::NotFound));
539    /// assert_eq!(eq.set_concentration_by_name("H2O", f32::NAN), Err(ConcentrationNameError::NAN));
540    /// ```
541    pub fn set_concentration_by_name(
542        &mut self,
543        name: &str,
544        concentration: f32,
545    ) -> Result<(), ConcentrationNameError> {
546        if concentration.is_nan() {
547            return Err(ConcentrationNameError::NAN);
548        }
549        // I don't like the collecting here...
550        // but I can't avoid double borrowing self as mutable and immutable
551        let (_name, cmp) = self
552            .compound_names()
553            .map(ToString::to_string)
554            .collect_vec()
555            .into_iter()
556            .zip(self.iter_compounds_mut())
557            .find(|(n, _c)| *n == name)
558            .ok_or(ConcentrationNameError::NotFound)?;
559        cmp.concentration = concentration;
560        Ok(())
561    }
562
563    /// Get the k expression or Kc of an equation. Returns [`None`] if one side
564    /// has a compound with a concentration of 0
565    ///
566    /// # Examples
567    ///
568    /// ```rust
569    /// use chem_eq::Equation;
570    ///
571    /// let eq = Equation::new("H2 + O2 -> H2O").unwrap();
572    /// // is nan because all compounds have an initial concentration of 0M
573    /// assert!(eq.equilibrium_constant().is_none());
574    ///
575    /// let mut eq = Equation::new("N2 + 2O2 -> 2NO2").unwrap();
576    /// eq.set_concentrations(&[0.25, 0.50, 0.75]).unwrap();
577    /// assert_eq!(eq.equilibrium_constant().unwrap(), (0.75 * 0.75) / (0.25 * 0.5 * 0.5));
578    /// ```
579    pub fn equilibrium_constant(&self) -> Option<f32> {
580        match self.reaction_quotient() {
581            ReactionQuotient::Val(q) => Some(q),
582            _ => None,
583        }
584    }
585
586    /// Get Qc of the equation, called the reaction coefficient. It's
587    /// calculated the same way as the k-expression, however it can apply to non
588    /// equilibrium concentrations.
589    ///
590    /// ## Returns
591    ///
592    /// Assuming the hypothetical reaction where a, b, c, and d are the
593    /// coefficents of A, B, C and D respectively:
594    /// ```text
595    /// aA + bB <-> cC + dD
596    /// ```
597    ///
598    /// Since `Qc = ([C]^c * [D]^d) / ([A]^a * [B]^b)`,
599    /// This function will return:
600    /// - [`ReactionQuotient::BothSidesZero`] if `[C]^c * [D]^d` and `[A]^a * [B]^b` are both 0
601    /// - [`ReactionQuotient::LeftZero`] if `[A]^a * [B]^b` is 0
602    /// - [`ReactionQuotient::RightZero`] if `[C]^c * [D]^d` is 0
603    /// - otherwise the result of the above equation contained in [`ReactionQuotient::Val`]
604    pub fn reaction_quotient(&self) -> ReactionQuotient {
605        // skip compounds that are solid or liquid
606        let left = self
607            .left
608            .iter()
609            .filter(|c| matches!(c.state, Some(State::Aqueous | State::Gas) | None))
610            .fold(1.0, |acc, cmp| {
611                acc * cmp.concentration.powf(cmp.coefficient as f32)
612            });
613
614        let right = self
615            .right
616            .iter()
617            .filter(|c| matches!(c.state, Some(State::Aqueous | State::Gas) | None))
618            .fold(1.0, |acc, cmp| {
619                acc * cmp.concentration.powf(cmp.coefficient as f32)
620            });
621
622        if left == 0.0 && right == 0.0 {
623            ReactionQuotient::BothSidesZero
624        } else if right == 0.0 {
625            ReactionQuotient::RightZero
626        } else if left == 0.0 {
627            ReactionQuotient::LeftZero
628        } else {
629            ReactionQuotient::Val(right / left)
630        }
631    }
632
633    /// Get the nth compound of the equation.
634    ///
635    /// ## Examples
636    ///
637    /// ```rust
638    /// use chem_eq::{Equation, Compound};
639    ///
640    /// let eq = Equation::new("H2 + O2 -> H2O").unwrap();
641    /// assert_eq!(eq.nth_compound(0).unwrap(), &Compound::parse("H2").unwrap());
642    /// assert_eq!(eq.nth_compound(1).unwrap(), &Compound::parse("O2").unwrap());
643    /// assert_eq!(eq.nth_compound(2).unwrap(), &Compound::parse("H2O").unwrap());
644    /// assert!(eq.nth_compound(3).is_none());
645    /// ```
646    pub fn nth_compound(&self, idx: usize) -> Option<&Compound> {
647        if idx < self.left.len() {
648            Some(&self.left[idx])
649        } else if idx < self.left.len() + self.right.len() {
650            Some(&self.right[idx - self.left.len()])
651        } else {
652            None
653        }
654    }
655
656    /// Get the nth compound of the equation mutably.
657    ///
658    /// ## Examples
659    ///
660    /// ```rust
661    /// use chem_eq::{Equation, Compound};
662    ///
663    /// let mut eq = Equation::new("H2 + O2 -> H2O").unwrap();
664    /// assert_eq!(eq.nth_compound_mut(0).unwrap(), &mut Compound::parse("H2").unwrap());
665    /// assert_eq!(eq.nth_compound_mut(1).unwrap(), &mut Compound::parse("O2").unwrap());
666    /// assert_eq!(eq.nth_compound_mut(2).unwrap(), &mut Compound::parse("H2O").unwrap());
667    /// assert!(eq.nth_compound_mut(3).is_none());
668    /// ```
669    pub fn nth_compound_mut(&mut self, idx: usize) -> Option<&mut Compound> {
670        if idx < self.left.len() {
671            Some(&mut self.left[idx])
672        } else if idx < self.left.len() + self.right.len() {
673            Some(&mut self.right[idx - self.left.len()])
674        } else {
675            None
676        }
677    }
678
679    /// Getter for the left side of the equation.
680    pub fn left(&self) -> &[Compound] {
681        self.left.as_ref()
682    }
683
684    /// Mut getter for the right side of the equation
685    pub fn left_mut(&mut self) -> &mut Vec<Compound> {
686        &mut self.left
687    }
688
689    /// Getter for the right side of the equation
690    pub fn right(&self) -> &[Compound] {
691        self.right.as_ref()
692    }
693
694    /// Mut getter for the right side of the equation
695    pub fn right_mut(&mut self) -> &mut Vec<Compound> {
696        &mut self.right
697    }
698
699    /// Getter for the direction of the equation
700    pub const fn direction(&self) -> &Direction {
701        &self.direction
702    }
703
704    /// Getter for the equation as text
705    pub fn equation(&self) -> &str {
706        self.equation.as_ref()
707    }
708
709    /// Getter for `delta_h` in kJ
710    pub const fn delta_h(&self) -> f32 {
711        self.delta_h
712    }
713
714    /// Setter for `delta_h` in kJ
715    pub fn set_delta_h(&mut self, delta_h: f32) {
716        self.delta_h = delta_h;
717    }
718
719    /// Getter for `temperature` in degrees Celsius
720    pub const fn temperature(&self) -> Option<f32> {
721        self.temperature
722    }
723
724    /// Setter for `temperature` in degrees Celsius
725    pub fn set_temperature(&mut self, temperature: f32) {
726        self.temperature = Some(temperature);
727    }
728
729    /// Getter for the `volume` equation in litres
730    pub const fn volume(&self) -> Option<f32> {
731        self.volume
732    }
733
734    /// Setter for `volume` in Litres
735    pub fn set_volume(&mut self, volume: f32) {
736        self.volume = Some(volume);
737    }
738}
739
740/// The result of [`Equation::reaction_quotient`].
741#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
742#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
743pub enum ReactionQuotient {
744    /// The product of the concentration of the reactants was zero
745    LeftZero,
746    /// The product of the concentration of the products was zero
747    RightZero,
748    /// Both the reactant and products had product concentrations of zero
749    BothSidesZero,
750    /// The reaction quotient
751    Val(f32),
752}
753
754#[cfg(test)]
755mod tests {
756    use super::*;
757
758    #[test]
759    fn too_much_input() {
760        assert_eq!(
761            Equation::new("H2 + O2 -> H2O-2wowthisistoolong"),
762            Err(EquationError::TooMuchInput(
763                "-2wowthisistoolong".to_string()
764            ))
765        )
766    }
767
768    #[test]
769    fn mol_ratio_basic() {
770        let eq = Equation::new("2O2 + H2 -> H2O").unwrap();
771        assert_eq!(eq.mol_ratio(), (3, 1));
772    }
773
774    #[test]
775    fn mol_ratio_states() {
776        let eq = Equation::new("2O2(g) + H2(g) -> H2O(l)").unwrap();
777        assert_eq!(eq.mol_ratio(), (3, 0));
778    }
779
780    #[test]
781    fn mol_ratio_more_states() {
782        // doesn't matter how bad an equation this is...
783        let eq = Equation::new("4FeH3(s) + 3O2(g) -> 2Fe2O3(s) + 6H2(g)").unwrap();
784        assert_eq!(eq.mol_ratio(), (3, 6));
785    }
786
787    #[test]
788    fn mol_ratio_no_aq() {
789        // doesn't matter how bad an equation this is...
790        // this one is _really_ bad though...
791        let eq = Equation::new("Fe(s) + K2(s) -> FeK(l)").unwrap();
792        assert_eq!(eq.mol_ratio(), (1, 1));
793    }
794
795    #[test]
796    fn uniq_elements_no_repeat() {
797        let eq = Equation::new("2O2 + H2 -> 2H2O").unwrap();
798        assert_eq!(eq.uniq_elements().len(), 2);
799    }
800
801    #[test]
802    fn uniq_elements_repeat() {
803        let eq = Equation::new("C + 2H2O -> CO2 + 2H2").unwrap();
804        assert_eq!(eq.uniq_elements().len(), 3);
805    }
806
807    #[test]
808    fn uniq_long() {
809        let eq =
810            Equation::new("3(NH4)2SO4(aq) + Fe3(PO4)2(s) <- 2(NH4)3PO4(aq) + 3FeSO4(aq)").unwrap();
811        assert_eq!(eq.uniq_elements().len(), 6);
812    }
813
814    #[test]
815    fn num_compounds_short() {
816        let eq = Equation::new("O2 + 2H2 -> 2H2O").unwrap();
817        assert_eq!(eq.num_compounds(), 3);
818    }
819
820    #[test]
821    fn num_compounds_long() {
822        let eq =
823            Equation::new("3(NH4)2SO4(aq) + Fe3(PO4)2(s) <- 2(NH4)3PO4(aq) + 3FeSO4(aq)").unwrap();
824        assert_eq!(eq.num_compounds(), 4);
825    }
826
827    #[test]
828    #[cfg(feature = "balance")]
829    fn is_balanced_correct() {
830        let eq = Equation::new("C + 2H2O -> CO2 + 2H2").unwrap();
831        assert!(eq.is_balanced());
832    }
833
834    #[test]
835    #[cfg(feature = "balance")]
836    fn is_balanced_incorrect() {
837        let eq = Equation::new("Mg(OH)2 + Fe -> Fe(OH)3 + Mg").unwrap();
838        assert!(!eq.is_balanced());
839    }
840}