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#[derive(Clone, Debug, Error, PartialEq, Eq)]
10pub enum QuantumNumberConversionError {
11 #[error("{kind} has no single numeric value")]
13 Ambiguous {
14 kind: &'static str,
16 },
17 #[error("{kind} is unknown")]
19 Unknown {
20 kind: &'static str,
22 },
23 #[error("{kind} has custom value {value:?}")]
25 Custom {
26 kind: &'static str,
28 value: String,
30 },
31 #[error("{kind} value {numerator}/{denominator} cannot be represented as {target}")]
33 OutOfRange {
34 kind: &'static str,
36 numerator: i32,
38 denominator: i32,
40 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}