Skip to main content

use_resistor/
lib.rs

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
9/// Commonly used resistor primitives.
10pub mod prelude {
11    pub use crate::{
12        ResistanceValue, ResistanceValueError, ResistorKind, ResistorKindParseError, ResistorSpec,
13    };
14}
15
16/// A resistance value in ohms.
17#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
18pub struct ResistanceValue {
19    ohms: f64,
20}
21
22impl ResistanceValue {
23    /// Creates a non-negative resistance value in ohms.
24    ///
25    /// # Errors
26    ///
27    /// Returns [`ResistanceValueError`] when the value is not finite or is negative.
28    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    /// Returns the value in ohms.
41    #[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/// Errors returned while constructing resistance values.
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum ResistanceValueError {
56    /// The resistance was not finite.
57    NonFinite,
58    /// The resistance was negative.
59    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/// Descriptive resistor kind vocabulary.
74#[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/// Errors returned while parsing resistor kinds.
130#[derive(Clone, Copy, Debug, Eq, PartialEq)]
131pub enum ResistorKindParseError {
132    /// The resistor kind was empty after trimming whitespace.
133    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/// A descriptive resistor specification.
147#[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    /// Creates a resistor spec from resistance and kind.
157    #[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    /// Returns the resistance value.
168    #[must_use]
169    pub const fn resistance(&self) -> ResistanceValue {
170        self.resistance
171    }
172
173    /// Returns the resistor kind.
174    #[must_use]
175    pub fn kind(&self) -> ResistorKind {
176        self.kind.clone()
177    }
178
179    /// Returns the optional tolerance.
180    #[must_use]
181    pub const fn tolerance(&self) -> Option<Tolerance> {
182        self.tolerance
183    }
184
185    /// Returns the optional power rating.
186    #[must_use]
187    pub const fn power_rating(&self) -> Option<PowerRating> {
188        self.power_rating
189    }
190
191    /// Returns this spec with a tolerance attached.
192    #[must_use]
193    pub const fn with_tolerance(mut self, tolerance: Tolerance) -> Self {
194        self.tolerance = Some(tolerance);
195        self
196    }
197
198    /// Returns this spec with a power rating attached.
199    #[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}