Skip to main content

use_magnitude/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn normalized_key(value: &str) -> String {
8    value
9        .trim()
10        .chars()
11        .map(|character| match character {
12            '_' | ' ' => '-',
13            other => other.to_ascii_lowercase(),
14        })
15        .collect()
16}
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum MagnitudeError {
20    NonFiniteMagnitude,
21    NonFiniteColorIndex,
22}
23
24impl fmt::Display for MagnitudeError {
25    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::NonFiniteMagnitude => formatter.write_str("magnitude must be finite"),
28            Self::NonFiniteColorIndex => formatter.write_str("color index must be finite"),
29        }
30    }
31}
32
33impl Error for MagnitudeError {}
34
35#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
36pub struct Magnitude(f64);
37
38impl Magnitude {
39    /// Creates a magnitude from a finite numeric value.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`MagnitudeError::NonFiniteMagnitude`] when `value` is not finite.
44    pub const fn new(value: f64) -> Result<Self, MagnitudeError> {
45        if !value.is_finite() {
46            return Err(MagnitudeError::NonFiniteMagnitude);
47        }
48
49        Ok(Self(value))
50    }
51
52    #[must_use]
53    pub const fn value(self) -> f64 {
54        self.0
55    }
56}
57
58impl fmt::Display for Magnitude {
59    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60        self.value().fmt(formatter)
61    }
62}
63
64#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
65pub enum MagnitudeKind {
66    Apparent,
67    Absolute,
68    Bolometric,
69    Visual,
70    Photographic,
71    Unknown,
72    Custom(String),
73}
74
75impl fmt::Display for MagnitudeKind {
76    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            Self::Apparent => formatter.write_str("apparent"),
79            Self::Absolute => formatter.write_str("absolute"),
80            Self::Bolometric => formatter.write_str("bolometric"),
81            Self::Visual => formatter.write_str("visual"),
82            Self::Photographic => formatter.write_str("photographic"),
83            Self::Unknown => formatter.write_str("unknown"),
84            Self::Custom(value) => formatter.write_str(value),
85        }
86    }
87}
88
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub enum MagnitudeKindParseError {
91    Empty,
92}
93
94impl fmt::Display for MagnitudeKindParseError {
95    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96        match self {
97            Self::Empty => formatter.write_str("magnitude kind cannot be empty"),
98        }
99    }
100}
101
102impl Error for MagnitudeKindParseError {}
103
104impl FromStr for MagnitudeKind {
105    type Err = MagnitudeKindParseError;
106
107    fn from_str(value: &str) -> Result<Self, Self::Err> {
108        let trimmed = value.trim();
109
110        if trimmed.is_empty() {
111            return Err(MagnitudeKindParseError::Empty);
112        }
113
114        match normalized_key(trimmed).as_str() {
115            "apparent" => Ok(Self::Apparent),
116            "absolute" => Ok(Self::Absolute),
117            "bolometric" => Ok(Self::Bolometric),
118            "visual" => Ok(Self::Visual),
119            "photographic" => Ok(Self::Photographic),
120            "unknown" => Ok(Self::Unknown),
121            _ => Ok(Self::Custom(trimmed.to_string())),
122        }
123    }
124}
125
126#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
127pub struct ColorIndex(f64);
128
129impl ColorIndex {
130    /// Creates a color index from a finite numeric value.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`MagnitudeError::NonFiniteColorIndex`] when `value` is not finite.
135    pub const fn new(value: f64) -> Result<Self, MagnitudeError> {
136        if !value.is_finite() {
137            return Err(MagnitudeError::NonFiniteColorIndex);
138        }
139
140        Ok(Self(value))
141    }
142
143    #[must_use]
144    pub const fn value(self) -> f64 {
145        self.0
146    }
147}
148
149impl fmt::Display for ColorIndex {
150    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
151        self.value().fmt(formatter)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::{ColorIndex, Magnitude, MagnitudeError, MagnitudeKind};
158
159    #[test]
160    fn valid_positive_magnitude() {
161        let magnitude = Magnitude::new(2.1).unwrap();
162
163        assert!((magnitude.value() - 2.1).abs() < f64::EPSILON);
164    }
165
166    #[test]
167    fn valid_negative_magnitude() {
168        let magnitude = Magnitude::new(-1.46).unwrap();
169
170        assert!((magnitude.value() - -1.46).abs() < f64::EPSILON);
171    }
172
173    #[test]
174    fn magnitude_kind_display_and_parse() {
175        assert_eq!(MagnitudeKind::Apparent.to_string(), "apparent");
176        assert_eq!(
177            "visual".parse::<MagnitudeKind>().unwrap(),
178            MagnitudeKind::Visual
179        );
180    }
181
182    #[test]
183    fn custom_magnitude_kind() {
184        assert_eq!(
185            "infrared-band".parse::<MagnitudeKind>().unwrap(),
186            MagnitudeKind::Custom("infrared-band".to_string())
187        );
188    }
189
190    #[test]
191    fn color_index_construction() {
192        let color_index = ColorIndex::new(0.65).unwrap();
193
194        assert!((color_index.value() - 0.65).abs() < f64::EPSILON);
195    }
196
197    #[test]
198    fn rejects_non_finite_values() {
199        assert_eq!(
200            Magnitude::new(f64::NAN),
201            Err(MagnitudeError::NonFiniteMagnitude)
202        );
203        assert_eq!(
204            ColorIndex::new(f64::INFINITY),
205            Err(MagnitudeError::NonFiniteColorIndex)
206        );
207    }
208}