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}