Skip to main content

pdg_rs/models/
conversions.rs

1#[cfg(feature = "ratio")]
2use std::{any::type_name, fmt::Display};
3
4use thiserror::Error;
5
6use super::pdgparticle::{AngularMomentum, Charge, Isospin, Parity};
7
8/// Error returned when a quantum number cannot be converted to a numeric type.
9#[derive(Clone, Debug, Error, PartialEq, Eq)]
10pub enum QuantumNumberConversionError {
11    /// The source value represents more than one possible numeric value.
12    #[error("{kind} has no single numeric value")]
13    Ambiguous {
14        /// Kind of quantum number being converted.
15        kind: &'static str,
16    },
17    /// The source value is explicitly unknown.
18    #[error("{kind} is unknown")]
19    Unknown {
20        /// Kind of quantum number being converted.
21        kind: &'static str,
22    },
23    /// The source value is a custom string that cannot be parsed as a number.
24    #[error("{kind} has custom value {value:?}")]
25    Custom {
26        /// Kind of quantum number being converted.
27        kind: &'static str,
28        /// Custom value that could not be converted.
29        value: String,
30    },
31    /// The rational value cannot be represented by the requested target type.
32    #[error("{kind} value {numerator}/{denominator} cannot be represented as {target}")]
33    OutOfRange {
34        /// Kind of quantum number being converted.
35        kind: &'static str,
36        /// Rational numerator before conversion.
37        numerator: i32,
38        /// Rational denominator before conversion.
39        denominator: i32,
40        /// Target numeric type name.
41        target: &'static str,
42    },
43}
44
45trait RationalParts {
46    fn kind(&self) -> &'static str;
47    fn rational_parts(&self) -> Result<(i32, i32), QuantumNumberConversionError>;
48}
49
50impl RationalParts for Charge {
51    fn kind(&self) -> &'static str {
52        "charge"
53    }
54
55    fn rational_parts(&self) -> Result<(i32, i32), QuantumNumberConversionError> {
56        Ok(match self {
57            Self::PlusPlus => (2, 1),
58            Self::Plus => (1, 1),
59            Self::Neutral => (0, 1),
60            Self::Minus => (-1, 1),
61            Self::MinusMinus => (-2, 1),
62            Self::PlusOneThird => (1, 3),
63            Self::PlusTwoThirds => (2, 3),
64            Self::MinusOneThird => (-1, 3),
65            Self::MinusTwoThirds => (-2, 3),
66        })
67    }
68}
69
70impl RationalParts for Isospin {
71    fn kind(&self) -> &'static str {
72        "isospin"
73    }
74
75    fn rational_parts(&self) -> Result<(i32, i32), QuantumNumberConversionError> {
76        match self {
77            Self::I0 => Ok((0, 1)),
78            Self::I1 => Ok((1, 2)),
79            Self::I2 => Ok((1, 1)),
80            Self::I3 => Ok((3, 2)),
81            Self::Photon => Err(QuantumNumberConversionError::Ambiguous { kind: self.kind() }),
82            Self::Unknown => Err(QuantumNumberConversionError::Unknown { kind: self.kind() }),
83        }
84    }
85}
86
87impl RationalParts for AngularMomentum {
88    fn kind(&self) -> &'static str {
89        "angular momentum"
90    }
91
92    fn rational_parts(&self) -> Result<(i32, i32), QuantumNumberConversionError> {
93        Ok(match self {
94            Self::J0 => (0, 1),
95            Self::J1 => (1, 2),
96            Self::J2 => (1, 1),
97            Self::J3 => (3, 2),
98            Self::J4 => (2, 1),
99            Self::J5 => (5, 2),
100            Self::J6 => (3, 1),
101            Self::J7 => (7, 2),
102            Self::J8 => (4, 1),
103            Self::J9 => (9, 2),
104            Self::J10 => (5, 1),
105            Self::J11 => (11, 2),
106            Self::J12 => (6, 1),
107            Self::J13 => (13, 2),
108            Self::J14 => (7, 1),
109            Self::J15 => (15, 2),
110            Self::Custom(value) => {
111                return Err(QuantumNumberConversionError::Custom {
112                    kind: self.kind(),
113                    value: value.clone(),
114                });
115            }
116            Self::Unknown => {
117                return Err(QuantumNumberConversionError::Unknown { kind: self.kind() });
118            }
119        })
120    }
121}
122
123impl RationalParts for Parity {
124    fn kind(&self) -> &'static str {
125        "parity"
126    }
127
128    fn rational_parts(&self) -> Result<(i32, i32), QuantumNumberConversionError> {
129        match self {
130            Self::Plus => Ok((1, 1)),
131            Self::Minus => Ok((-1, 1)),
132            Self::Unknown => Err(QuantumNumberConversionError::Unknown { kind: self.kind() }),
133        }
134    }
135}
136
137fn to_f64<T: RationalParts>(value: &T) -> Result<f64, QuantumNumberConversionError> {
138    let (numerator, denominator) = value.rational_parts()?;
139    Ok(f64::from(numerator) / f64::from(denominator))
140}
141
142macro_rules! impl_f64_conversion {
143    ($source:ty) => {
144        impl TryFrom<$source> for f64 {
145            type Error = QuantumNumberConversionError;
146
147            fn try_from(value: $source) -> Result<Self, Self::Error> {
148                to_f64(&value)
149            }
150        }
151
152        impl TryFrom<&$source> for f64 {
153            type Error = QuantumNumberConversionError;
154
155            fn try_from(value: &$source) -> Result<Self, Self::Error> {
156                to_f64(value)
157            }
158        }
159    };
160}
161
162impl_f64_conversion!(Charge);
163impl_f64_conversion!(Isospin);
164impl_f64_conversion!(AngularMomentum);
165impl_f64_conversion!(Parity);
166
167#[cfg(feature = "ratio")]
168fn to_ratio<T, V>(value: &V) -> Result<num::rational::Ratio<T>, QuantumNumberConversionError>
169where
170    T: num::Integer + num::traits::NumCast + Clone + Display,
171    V: RationalParts,
172{
173    let (raw_numerator, raw_denominator) = value.rational_parts()?;
174    let numerator =
175        T::from(raw_numerator).ok_or_else(|| QuantumNumberConversionError::OutOfRange {
176            kind: value.kind(),
177            numerator: raw_numerator,
178            denominator: raw_denominator,
179            target: type_name::<T>(),
180        })?;
181    let denominator =
182        T::from(raw_denominator).ok_or_else(|| QuantumNumberConversionError::OutOfRange {
183            kind: value.kind(),
184            numerator: raw_numerator,
185            denominator: raw_denominator,
186            target: type_name::<T>(),
187        })?;
188    Ok(num::rational::Ratio::new(numerator, denominator))
189}
190
191#[cfg(feature = "ratio")]
192macro_rules! impl_ratio_conversion {
193    ($source:ty) => {
194        impl<T> TryFrom<$source> for num::rational::Ratio<T>
195        where
196            T: num::Integer + num::traits::NumCast + Clone + Display,
197        {
198            type Error = QuantumNumberConversionError;
199
200            fn try_from(value: $source) -> Result<Self, Self::Error> {
201                to_ratio(&value)
202            }
203        }
204
205        impl<T> TryFrom<&$source> for num::rational::Ratio<T>
206        where
207            T: num::Integer + num::traits::NumCast + Clone + Display,
208        {
209            type Error = QuantumNumberConversionError;
210
211            fn try_from(value: &$source) -> Result<Self, Self::Error> {
212                to_ratio(value)
213            }
214        }
215    };
216}
217
218#[cfg(feature = "ratio")]
219impl_ratio_conversion!(Charge);
220#[cfg(feature = "ratio")]
221impl_ratio_conversion!(Isospin);
222#[cfg(feature = "ratio")]
223impl_ratio_conversion!(AngularMomentum);
224#[cfg(feature = "ratio")]
225impl_ratio_conversion!(Parity);
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    #[allow(clippy::float_cmp)]
233    fn converts_quantum_numbers_to_f64() {
234        assert_eq!(f64::try_from(Charge::MinusOneThird).unwrap(), -1.0 / 3.0);
235        assert_eq!(f64::try_from(Isospin::I3).unwrap(), 1.5);
236        assert_eq!(f64::try_from(AngularMomentum::J5).unwrap(), 2.5);
237        assert_eq!(f64::try_from(Parity::Minus).unwrap(), -1.0);
238    }
239
240    #[test]
241    fn rejects_unknown_ambiguous_and_custom_values() {
242        assert_eq!(
243            f64::try_from(Isospin::Photon).unwrap_err(),
244            QuantumNumberConversionError::Ambiguous { kind: "isospin" }
245        );
246        assert_eq!(
247            f64::try_from(Parity::Unknown).unwrap_err(),
248            QuantumNumberConversionError::Unknown { kind: "parity" }
249        );
250        assert_eq!(
251            f64::try_from(AngularMomentum::Custom("1 or 2".to_string())).unwrap_err(),
252            QuantumNumberConversionError::Custom {
253                kind: "angular momentum",
254                value: "1 or 2".to_string()
255            }
256        );
257    }
258
259    #[cfg(feature = "ratio")]
260    #[test]
261    fn converts_quantum_numbers_to_ratios() {
262        let charge: num::rational::Ratio<i8> = Charge::MinusOneThird.try_into().unwrap();
263        let spin: num::rational::Ratio<u8> = AngularMomentum::J5.try_into().unwrap();
264        let isospin: num::rational::Ratio<usize> = Isospin::I3.try_into().unwrap();
265
266        assert_eq!(charge, num::rational::Ratio::new(-1, 3));
267        assert_eq!(spin, num::rational::Ratio::new(5, 2));
268        assert_eq!(isospin, num::rational::Ratio::new(3, 2));
269    }
270
271    #[cfg(feature = "ratio")]
272    #[test]
273    fn rejects_negative_values_for_unsigned_ratios() {
274        let error = num::rational::Ratio::<u8>::try_from(Charge::Minus).unwrap_err();
275
276        assert_eq!(
277            error,
278            QuantumNumberConversionError::OutOfRange {
279                kind: "charge",
280                numerator: -1,
281                denominator: 1,
282                target: "u8"
283            }
284        );
285    }
286}