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 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 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}