Skip to main content

use_compound/
compound.rs

1use std::fmt;
2
3use use_chemical_formula::ChemicalFormula;
4
5use crate::{
6    CommonName, CompoundFormula, CompoundIdentifier, CompoundKind, CompoundName,
7    CompoundValidationError, EmpiricalFormula, MolecularFormula, SystematicName,
8};
9
10/// A named chemical compound with formula and lightweight descriptors.
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct Compound {
13    name: CompoundName,
14    formula: CompoundFormula,
15    common_name: Option<CommonName>,
16    systematic_name: Option<SystematicName>,
17    empirical_formula: Option<EmpiricalFormula>,
18    molecular_formula: Option<MolecularFormula>,
19    kinds: Vec<CompoundKind>,
20    identifiers: Vec<CompoundIdentifier>,
21}
22
23impl Compound {
24    /// Creates a compound from a name and formula.
25    ///
26    /// # Errors
27    ///
28    /// Returns [`CompoundValidationError::EmptyName`] when `name` is empty after trimming.
29    pub fn new(name: &str, formula: ChemicalFormula) -> Result<Self, CompoundValidationError> {
30        Ok(Self {
31            name: CompoundName::new(name)?,
32            formula: CompoundFormula::new(formula),
33            common_name: None,
34            systematic_name: None,
35            empirical_formula: None,
36            molecular_formula: None,
37            kinds: Vec::new(),
38            identifiers: Vec::new(),
39        })
40    }
41
42    /// Returns the primary compound name.
43    #[must_use]
44    pub const fn name(&self) -> &CompoundName {
45        &self.name
46    }
47
48    /// Returns the primary formula.
49    #[must_use]
50    pub fn formula(&self) -> &ChemicalFormula {
51        self.formula.as_formula()
52    }
53
54    /// Returns the compound formula wrapper.
55    #[must_use]
56    pub const fn compound_formula(&self) -> &CompoundFormula {
57        &self.formula
58    }
59
60    /// Returns the optional common name.
61    #[must_use]
62    pub const fn common_name(&self) -> Option<&CommonName> {
63        self.common_name.as_ref()
64    }
65
66    /// Returns the optional systematic name.
67    #[must_use]
68    pub const fn systematic_name(&self) -> Option<&SystematicName> {
69        self.systematic_name.as_ref()
70    }
71
72    /// Returns the optional empirical formula.
73    #[must_use]
74    pub const fn empirical_formula(&self) -> Option<&EmpiricalFormula> {
75        self.empirical_formula.as_ref()
76    }
77
78    /// Returns the optional molecular formula.
79    #[must_use]
80    pub const fn molecular_formula(&self) -> Option<&MolecularFormula> {
81        self.molecular_formula.as_ref()
82    }
83
84    /// Returns compound kind labels in insertion order.
85    #[must_use]
86    pub fn kinds(&self) -> &[CompoundKind] {
87        &self.kinds
88    }
89
90    /// Returns compound identifiers in insertion order.
91    #[must_use]
92    pub fn identifiers(&self) -> &[CompoundIdentifier] {
93        &self.identifiers
94    }
95
96    /// Adds a kind label if it is not already present.
97    #[must_use]
98    pub fn with_kind(mut self, kind: CompoundKind) -> Self {
99        if !self.kinds.contains(&kind) {
100            self.kinds.push(kind);
101        }
102        self
103    }
104
105    /// Sets the common name from a validated value.
106    #[must_use]
107    pub fn with_common_name(mut self, common_name: CommonName) -> Self {
108        self.common_name = Some(common_name);
109        self
110    }
111
112    /// Sets the common name after validation.
113    ///
114    /// # Errors
115    ///
116    /// Returns [`CompoundValidationError::EmptyCommonName`] when `common_name` is empty after trimming.
117    pub fn try_with_common_name(self, common_name: &str) -> Result<Self, CompoundValidationError> {
118        Ok(self.with_common_name(CommonName::new(common_name)?))
119    }
120
121    /// Sets the systematic name from a validated value.
122    #[must_use]
123    pub fn with_systematic_name(mut self, systematic_name: SystematicName) -> Self {
124        self.systematic_name = Some(systematic_name);
125        self
126    }
127
128    /// Sets the systematic name after validation.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`CompoundValidationError::EmptySystematicName`] when `systematic_name` is empty after trimming.
133    pub fn try_with_systematic_name(
134        self,
135        systematic_name: &str,
136    ) -> Result<Self, CompoundValidationError> {
137        Ok(self.with_systematic_name(SystematicName::new(systematic_name)?))
138    }
139
140    /// Sets the empirical formula.
141    #[must_use]
142    pub fn with_empirical_formula(mut self, formula: EmpiricalFormula) -> Self {
143        self.empirical_formula = Some(formula);
144        self
145    }
146
147    /// Sets the molecular formula.
148    #[must_use]
149    pub fn with_molecular_formula(mut self, formula: MolecularFormula) -> Self {
150        self.molecular_formula = Some(formula);
151        self
152    }
153
154    /// Adds an identifier if it is not already present.
155    ///
156    /// # Errors
157    ///
158    /// This method currently cannot fail because [`CompoundIdentifier`] values are validated at
159    /// construction, but it returns `Result` so builder chains can stay consistently fallible.
160    pub fn try_with_identifier(
161        mut self,
162        identifier: CompoundIdentifier,
163    ) -> Result<Self, CompoundValidationError> {
164        if !self.identifiers.contains(&identifier) {
165            self.identifiers.push(identifier);
166        }
167        Ok(self)
168    }
169}
170
171impl fmt::Display for Compound {
172    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173        write!(formatter, "{} ({})", self.name, self.formula)
174    }
175}