1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_rating::{PowerRating, Tolerance};
8
9pub mod prelude {
11 pub use crate::{
12 ResistanceValue, ResistanceValueError, ResistorKind, ResistorKindParseError, ResistorSpec,
13 };
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
18pub struct ResistanceValue {
19 ohms: f64,
20}
21
22impl ResistanceValue {
23 pub fn new_ohms(value: f64) -> Result<Self, ResistanceValueError> {
29 if !value.is_finite() {
30 return Err(ResistanceValueError::NonFinite);
31 }
32
33 if value < 0.0 {
34 return Err(ResistanceValueError::Negative);
35 }
36
37 Ok(Self { ohms: value })
38 }
39
40 #[must_use]
42 pub const fn ohms(self) -> f64 {
43 self.ohms
44 }
45}
46
47impl fmt::Display for ResistanceValue {
48 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49 write!(formatter, "{} ohm", self.ohms)
50 }
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum ResistanceValueError {
56 NonFinite,
58 Negative,
60}
61
62impl fmt::Display for ResistanceValueError {
63 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 Self::NonFinite => formatter.write_str("resistance must be finite"),
66 Self::Negative => formatter.write_str("resistance cannot be negative"),
67 }
68 }
69}
70
71impl Error for ResistanceValueError {}
72
73#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
75pub enum ResistorKind {
76 Fixed,
77 Variable,
78 Potentiometer,
79 Thermistor,
80 Photoresistor,
81 Shunt,
82 PullUp,
83 PullDown,
84 Unknown,
85 Custom(String),
86}
87
88impl fmt::Display for ResistorKind {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 formatter.write_str(match self {
91 Self::Fixed => "fixed",
92 Self::Variable => "variable",
93 Self::Potentiometer => "potentiometer",
94 Self::Thermistor => "thermistor",
95 Self::Photoresistor => "photoresistor",
96 Self::Shunt => "shunt",
97 Self::PullUp => "pull-up",
98 Self::PullDown => "pull-down",
99 Self::Unknown => "unknown",
100 Self::Custom(value) => value.as_str(),
101 })
102 }
103}
104
105impl FromStr for ResistorKind {
106 type Err = ResistorKindParseError;
107
108 fn from_str(value: &str) -> Result<Self, Self::Err> {
109 let trimmed = value.trim();
110 if trimmed.is_empty() {
111 return Err(ResistorKindParseError::Empty);
112 }
113
114 match normalized_token(trimmed).as_str() {
115 "fixed" => Ok(Self::Fixed),
116 "variable" => Ok(Self::Variable),
117 "potentiometer" => Ok(Self::Potentiometer),
118 "thermistor" => Ok(Self::Thermistor),
119 "photoresistor" => Ok(Self::Photoresistor),
120 "shunt" => Ok(Self::Shunt),
121 "pull-up" => Ok(Self::PullUp),
122 "pull-down" => Ok(Self::PullDown),
123 "unknown" => Ok(Self::Unknown),
124 _ => Ok(Self::Custom(trimmed.to_string())),
125 }
126 }
127}
128
129#[derive(Clone, Copy, Debug, Eq, PartialEq)]
131pub enum ResistorKindParseError {
132 Empty,
134}
135
136impl fmt::Display for ResistorKindParseError {
137 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
138 match self {
139 Self::Empty => formatter.write_str("resistor kind cannot be empty"),
140 }
141 }
142}
143
144impl Error for ResistorKindParseError {}
145
146#[derive(Clone, Debug, PartialEq)]
148pub struct ResistorSpec {
149 resistance: ResistanceValue,
150 kind: ResistorKind,
151 tolerance: Option<Tolerance>,
152 power_rating: Option<PowerRating>,
153}
154
155impl ResistorSpec {
156 #[must_use]
158 pub const fn new(resistance: ResistanceValue, kind: ResistorKind) -> Self {
159 Self {
160 resistance,
161 kind,
162 tolerance: None,
163 power_rating: None,
164 }
165 }
166
167 #[must_use]
169 pub const fn resistance(&self) -> ResistanceValue {
170 self.resistance
171 }
172
173 #[must_use]
175 pub fn kind(&self) -> ResistorKind {
176 self.kind.clone()
177 }
178
179 #[must_use]
181 pub const fn tolerance(&self) -> Option<Tolerance> {
182 self.tolerance
183 }
184
185 #[must_use]
187 pub const fn power_rating(&self) -> Option<PowerRating> {
188 self.power_rating
189 }
190
191 #[must_use]
193 pub const fn with_tolerance(mut self, tolerance: Tolerance) -> Self {
194 self.tolerance = Some(tolerance);
195 self
196 }
197
198 #[must_use]
200 pub const fn with_power_rating(mut self, power_rating: PowerRating) -> Self {
201 self.power_rating = Some(power_rating);
202 self
203 }
204}
205
206fn normalized_token(value: &str) -> String {
207 value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
208}
209
210#[cfg(test)]
211mod tests {
212 use super::{ResistanceValue, ResistanceValueError, ResistorKind, ResistorSpec};
213 use use_rating::{PowerRating, Tolerance};
214
215 #[test]
216 fn accepts_valid_resistance() -> Result<(), ResistanceValueError> {
217 let value = ResistanceValue::new_ohms(10_000.0)?;
218
219 assert!((value.ohms() - 10_000.0).abs() < f64::EPSILON);
220 Ok(())
221 }
222
223 #[test]
224 fn rejects_negative_resistance() {
225 assert_eq!(
226 ResistanceValue::new_ohms(-1.0),
227 Err(ResistanceValueError::Negative)
228 );
229 }
230
231 #[test]
232 fn displays_and_parses_resistor_kinds() -> Result<(), Box<dyn std::error::Error>> {
233 assert_eq!("pull up".parse::<ResistorKind>()?, ResistorKind::PullUp);
234 assert_eq!(ResistorKind::Photoresistor.to_string(), "photoresistor");
235 Ok(())
236 }
237
238 #[test]
239 fn builds_resistor_specs_with_tolerance() -> Result<(), Box<dyn std::error::Error>> {
240 let spec = ResistorSpec::new(ResistanceValue::new_ohms(1_000.0)?, ResistorKind::Fixed)
241 .with_tolerance(Tolerance::from_percent(5.0)?);
242
243 assert_eq!(spec.tolerance().map(Tolerance::percent), Some(5.0));
244 Ok(())
245 }
246
247 #[test]
248 fn builds_resistor_specs_with_power_rating() -> Result<(), Box<dyn std::error::Error>> {
249 let spec = ResistorSpec::new(ResistanceValue::new_ohms(10.0)?, ResistorKind::Shunt)
250 .with_power_rating(PowerRating::new_watts(1.0)?);
251
252 assert_eq!(spec.power_rating().map(PowerRating::watts), Some(1.0));
253 Ok(())
254 }
255}