Skip to main content

iridium_units/equivalencies/
mod.rs

1//! Equivalencies for converting between different physical domains.
2//!
3//! Equivalencies enable conversions between units with different dimensions
4//! when there is a physical relationship between them. For example:
5//!
6//! - Wavelength ↔ Frequency (via speed of light)
7//! - Mass ↔ Energy (via E=mc²)
8//! - Parallax angle ↔ Distance
9//!
10//! # Example
11//!
12//! ```no_run
13//! # #[cfg(feature = "astrophysics")]
14//! # fn main() {
15//! use iridium_units::prelude::*;
16//! use iridium_units::equivalencies::spectral;
17//!
18//! let wavelength = 500.0 * NM;
19//! let frequency = wavelength.to_equiv(HZ, spectral()).unwrap();
20//! # }
21//! # #[cfg(not(feature = "astrophysics"))]
22//! # fn main() {}
23//! ```
24
25#[cfg(feature = "astrophysics")]
26pub mod brightness_temperature;
27pub mod dimensionless_angles;
28#[cfg(feature = "astrophysics")]
29pub mod doppler;
30#[cfg(feature = "logarithmic")]
31pub mod logarithmic;
32pub mod mass_energy;
33#[cfg(feature = "astrophysics")]
34pub mod parallax;
35#[cfg(feature = "astrophysics")]
36pub mod spectral;
37#[cfg(feature = "astrophysics")]
38pub mod spectral_density;
39pub mod temperature;
40
41use crate::error::{UnitError, UnitResult};
42use crate::quantity::Quantity;
43use crate::unit::Unit;
44use std::sync::Arc;
45
46/// Type alias for the converter factory function used by equivalencies.
47pub type ConverterFn = Arc<dyn Fn(&Unit, &Unit) -> Option<Converter> + Send + Sync>;
48
49/// An equivalency that enables conversion between different physical dimensions.
50#[derive(Clone)]
51pub struct Equivalency {
52    name: &'static str,
53    converter_fn: ConverterFn,
54}
55
56impl Equivalency {
57    /// Create a new equivalency with a converter function.
58    pub fn new<F>(name: &'static str, converter_fn: F) -> Self
59    where
60        F: Fn(&Unit, &Unit) -> Option<Converter> + Send + Sync + 'static,
61    {
62        Equivalency {
63            name,
64            converter_fn: Arc::new(converter_fn),
65        }
66    }
67
68    /// Get the name of this equivalency.
69    pub fn name(&self) -> &str {
70        self.name
71    }
72
73    /// Try to create a converter between two units.
74    pub fn get_converter(&self, from: &Unit, to: &Unit) -> Option<Converter> {
75        (self.converter_fn)(from, to)
76    }
77}
78
79impl std::fmt::Debug for Equivalency {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        write!(f, "Equivalency({})", self.name())
82    }
83}
84
85/// A converter that can transform values between units.
86///
87/// Converters use `Result<f64, String>` to handle invalid inputs
88/// (e.g., zero wavelength, negative temperature, superluminal velocity).
89pub struct Converter {
90    forward: Box<dyn Fn(f64) -> Result<f64, String> + Send + Sync>,
91    backward: Box<dyn Fn(f64) -> Result<f64, String> + Send + Sync>,
92}
93
94impl Converter {
95    /// Create a new converter with forward and backward functions that can fail.
96    pub fn new<F, B>(forward: F, backward: B) -> Self
97    where
98        F: Fn(f64) -> Result<f64, String> + Send + Sync + 'static,
99        B: Fn(f64) -> Result<f64, String> + Send + Sync + 'static,
100    {
101        Converter {
102            forward: Box::new(forward),
103            backward: Box::new(backward),
104        }
105    }
106
107    /// Create a converter from infallible functions (for backwards compatibility).
108    pub fn new_infallible<F, B>(forward: F, backward: B) -> Self
109    where
110        F: Fn(f64) -> f64 + Send + Sync + 'static,
111        B: Fn(f64) -> f64 + Send + Sync + 'static,
112    {
113        Converter {
114            forward: Box::new(move |x| Ok(forward(x))),
115            backward: Box::new(move |x| Ok(backward(x))),
116        }
117    }
118
119    /// Apply the forward conversion.
120    pub fn convert(&self, value: f64) -> Result<f64, String> {
121        (self.forward)(value)
122    }
123
124    /// Apply the backward conversion.
125    pub fn convert_back(&self, value: f64) -> Result<f64, String> {
126        (self.backward)(value)
127    }
128}
129
130impl Quantity {
131    /// Convert to another unit using equivalencies.
132    ///
133    /// First tries a direct dimensional conversion. If that fails,
134    /// tries each equivalency in order until one succeeds.
135    pub fn to_equiv(&self, target: impl Into<Unit>, equiv: Equivalency) -> UnitResult<Quantity> {
136        self.to_equiv_list(target, &[equiv])
137    }
138
139    /// Convert to another unit using a list of equivalencies.
140    pub fn to_equiv_list(
141        &self,
142        target: impl Into<Unit>,
143        equivs: &[Equivalency],
144    ) -> UnitResult<Quantity> {
145        let target = target.into();
146        // First, try direct conversion (inline to avoid cloning target)
147        if self.unit().dimension() == target.dimension() {
148            let si_value = self.unit().to_si(self.value());
149            return Ok(Quantity::new(target.from_si(si_value), target));
150        }
151
152        // Try each equivalency
153        for equiv in equivs {
154            if let Some(converter) = equiv.get_converter(self.unit(), &target) {
155                // Convert to SI value first (handles offset units like °C)
156                let si_value = self.unit().to_si(self.value());
157                // Apply the equivalency conversion (may fail for invalid inputs)
158                let converted_si =
159                    converter
160                        .convert(si_value)
161                        .map_err(|msg| UnitError::NoEquivalency {
162                            from: format!("{} ({})", self.unit(), msg),
163                            to: target.to_string(),
164                        })?;
165                // Convert from SI to target unit (handles offset units like °C)
166                let target_value = target.from_si(converted_si);
167                return Ok(Quantity::new(target_value, target));
168            }
169        }
170
171        Err(UnitError::NoEquivalency {
172            from: self.unit().to_string(),
173            to: target.to_string(),
174        })
175    }
176}
177
178// Re-export commonly used equivalencies
179#[cfg(feature = "astrophysics")]
180pub use brightness_temperature::{
181    brightness_temperature, brightness_temperature_intensity, brightness_temperature_planck,
182};
183pub use dimensionless_angles::dimensionless_angles;
184#[cfg(feature = "astrophysics")]
185pub use doppler::{doppler_optical, doppler_radio, doppler_relativistic};
186#[cfg(feature = "logarithmic")]
187pub use logarithmic::{db_amplitude, db_power, dex_ratio, magnitude_flux};
188pub use mass_energy::mass_energy;
189#[cfg(feature = "astrophysics")]
190pub use parallax::parallax;
191#[cfg(feature = "astrophysics")]
192pub use spectral::spectral;
193#[cfg(feature = "astrophysics")]
194pub use spectral_density::{ab_magnitude, ab_magnitude_lambda, spectral_density};
195pub use temperature::{temperature, temperature_energy};