Skip to main content

use_electricity/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Electricity-specific scalar helpers.
5
6pub mod prelude;
7
8/// Coulomb's constant for electrostatic force calculations.
9///
10/// More general physical constants belong in the top-level `use-constants` set.
11pub const COULOMB_CONSTANT: f64 = 8.987_551_792_3e9;
12
13fn all_finite(values: &[f64]) -> bool {
14    values.iter().all(|value| value.is_finite())
15}
16
17fn finite_result(value: f64) -> Option<f64> {
18    value.is_finite().then_some(value)
19}
20
21/// Computes electric charge from current and elapsed time.
22#[must_use]
23pub fn charge_from_current_time(current: f64, time: f64) -> Option<f64> {
24    if !all_finite(&[current, time]) || time < 0.0 {
25        return None;
26    }
27
28    finite_result(current * time)
29}
30
31/// Computes current from electric charge and elapsed time.
32#[must_use]
33pub fn current_from_charge_time(charge: f64, time: f64) -> Option<f64> {
34    if !all_finite(&[charge, time]) || time <= 0.0 {
35        return None;
36    }
37
38    finite_result(charge / time)
39}
40
41/// Computes voltage from current and resistance using Ohm's law.
42///
43/// # Examples
44///
45/// ```
46/// use use_electricity::voltage;
47///
48/// assert_eq!(voltage(2.0, 5.0), Some(10.0));
49/// assert_eq!(voltage(2.0, -5.0), None);
50/// ```
51#[must_use]
52pub fn voltage(current: f64, resistance: f64) -> Option<f64> {
53    if !all_finite(&[current, resistance]) || resistance < 0.0 {
54        return None;
55    }
56
57    finite_result(current * resistance)
58}
59
60/// Computes current from voltage and resistance using Ohm's law.
61///
62/// # Examples
63///
64/// ```
65/// use use_electricity::current;
66///
67/// assert_eq!(current(10.0, 5.0), Some(2.0));
68/// assert_eq!(current(10.0, 0.0), None);
69/// ```
70#[must_use]
71pub fn current(voltage: f64, resistance: f64) -> Option<f64> {
72    if !all_finite(&[voltage, resistance]) || resistance <= 0.0 {
73        return None;
74    }
75
76    finite_result(voltage / resistance)
77}
78
79/// Computes resistance from voltage and current using Ohm's law.
80///
81/// # Examples
82///
83/// ```
84/// use use_electricity::resistance;
85///
86/// assert_eq!(resistance(10.0, 2.0), Some(5.0));
87/// assert_eq!(resistance(-10.0, 2.0), None);
88/// ```
89#[must_use]
90pub fn resistance(voltage: f64, current: f64) -> Option<f64> {
91    if !all_finite(&[voltage, current]) || current == 0.0 {
92        return None;
93    }
94
95    let resistance = voltage / current;
96    if !resistance.is_finite() || resistance < 0.0 {
97        None
98    } else {
99        Some(resistance)
100    }
101}
102
103/// Computes conductance from resistance.
104#[must_use]
105pub fn conductance(resistance: f64) -> Option<f64> {
106    if !resistance.is_finite() || resistance <= 0.0 {
107        return None;
108    }
109
110    finite_result(1.0 / resistance)
111}
112
113/// Computes resistance from conductance.
114#[must_use]
115pub fn resistance_from_conductance(conductance: f64) -> Option<f64> {
116    if !conductance.is_finite() || conductance <= 0.0 {
117        return None;
118    }
119
120    finite_result(1.0 / conductance)
121}
122
123/// Computes electrical power from voltage and current.
124///
125/// # Examples
126///
127/// ```
128/// use use_electricity::power_from_voltage_current;
129///
130/// assert_eq!(power_from_voltage_current(10.0, 2.0), Some(20.0));
131/// assert_eq!(power_from_voltage_current(-10.0, 2.0), Some(-20.0));
132/// ```
133#[must_use]
134pub fn power_from_voltage_current(voltage: f64, current: f64) -> Option<f64> {
135    if !all_finite(&[voltage, current]) {
136        return None;
137    }
138
139    finite_result(voltage * current)
140}
141
142/// Computes electrical power from current and resistance.
143#[must_use]
144pub fn power_from_current_resistance(current: f64, resistance: f64) -> Option<f64> {
145    if !all_finite(&[current, resistance]) || resistance < 0.0 {
146        return None;
147    }
148
149    finite_result(current * current * resistance)
150}
151
152/// Computes electrical power from voltage and resistance.
153#[must_use]
154pub fn power_from_voltage_resistance(voltage: f64, resistance: f64) -> Option<f64> {
155    if !all_finite(&[voltage, resistance]) || resistance <= 0.0 {
156        return None;
157    }
158
159    finite_result((voltage * voltage) / resistance)
160}
161
162/// Computes electrical energy from power and elapsed time.
163#[must_use]
164pub fn energy_from_power_time(power: f64, time: f64) -> Option<f64> {
165    if !all_finite(&[power, time]) || time < 0.0 {
166        return None;
167    }
168
169    finite_result(power * time)
170}
171
172/// Computes electrical energy from voltage and charge.
173#[must_use]
174pub fn energy_from_voltage_charge(voltage: f64, charge: f64) -> Option<f64> {
175    if !all_finite(&[voltage, charge]) {
176        return None;
177    }
178
179    finite_result(voltage * charge)
180}
181
182/// Computes the total resistance for resistors in series.
183///
184/// # Examples
185///
186/// ```
187/// use use_electricity::series_resistance;
188///
189/// assert_eq!(series_resistance(&[1.0, 2.0, 3.0]), Some(6.0));
190/// assert_eq!(series_resistance(&[]), Some(0.0));
191/// ```
192#[must_use]
193pub fn series_resistance(resistances: &[f64]) -> Option<f64> {
194    let mut total = 0.0;
195
196    for &resistance in resistances {
197        if !resistance.is_finite() || resistance < 0.0 {
198            return None;
199        }
200
201        total += resistance;
202    }
203
204    finite_result(total)
205}
206
207/// Computes the total resistance for resistors in parallel.
208///
209/// # Examples
210///
211/// ```
212/// use use_electricity::parallel_resistance;
213///
214/// assert_eq!(parallel_resistance(&[2.0, 2.0]), Some(1.0));
215/// assert_eq!(parallel_resistance(&[]), None);
216/// ```
217#[must_use]
218pub fn parallel_resistance(resistances: &[f64]) -> Option<f64> {
219    if resistances.is_empty() {
220        return None;
221    }
222
223    let mut reciprocal_sum = 0.0;
224
225    for &resistance in resistances {
226        if !resistance.is_finite() || resistance <= 0.0 {
227            return None;
228        }
229
230        reciprocal_sum += 1.0 / resistance;
231    }
232
233    finite_result(1.0 / reciprocal_sum)
234}
235
236/// Computes electrostatic force using Coulomb's law.
237///
238/// The sign is preserved so callers can distinguish attractive and repulsive
239/// direction conventions.
240///
241/// # Examples
242///
243/// ```
244/// use use_electricity::{COULOMB_CONSTANT, coulomb_force};
245///
246/// assert_eq!(coulomb_force(1.0, 1.0, 1.0), Some(COULOMB_CONSTANT));
247/// assert_eq!(coulomb_force(1.0, -1.0, 1.0), Some(-COULOMB_CONSTANT));
248/// ```
249#[must_use]
250pub fn coulomb_force(charge_a: f64, charge_b: f64, distance: f64) -> Option<f64> {
251    if !all_finite(&[charge_a, charge_b, distance]) || distance <= 0.0 {
252        return None;
253    }
254
255    finite_result(COULOMB_CONSTANT * charge_a * charge_b / (distance * distance))
256}
257
258/// A simple electrical load described by voltage and resistance.
259#[derive(Debug, Clone, Copy, PartialEq)]
260pub struct ElectricalLoad {
261    pub voltage: f64,
262    pub resistance: f64,
263}
264
265impl ElectricalLoad {
266    /// Creates a new electrical load from voltage and resistance.
267    #[must_use]
268    pub fn new(voltage: f64, resistance: f64) -> Option<Self> {
269        if !voltage.is_finite() || !resistance.is_finite() || resistance <= 0.0 {
270            return None;
271        }
272
273        Some(Self {
274            voltage,
275            resistance,
276        })
277    }
278
279    /// Computes the current drawn by the load.
280    #[must_use]
281    pub fn current(&self) -> Option<f64> {
282        current(self.voltage, self.resistance)
283    }
284
285    /// Computes the power consumed by the load.
286    ///
287    /// # Examples
288    ///
289    /// ```
290    /// use use_electricity::ElectricalLoad;
291    ///
292    /// let load = ElectricalLoad::new(10.0, 5.0).expect("valid load");
293    /// assert_eq!(load.power(), Some(20.0));
294    /// ```
295    #[must_use]
296    pub fn power(&self) -> Option<f64> {
297        power_from_voltage_resistance(self.voltage, self.resistance)
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::{
304        COULOMB_CONSTANT, ElectricalLoad, charge_from_current_time, conductance, coulomb_force,
305        current, current_from_charge_time, energy_from_power_time, energy_from_voltage_charge,
306        parallel_resistance, power_from_current_resistance, power_from_voltage_current,
307        power_from_voltage_resistance, resistance, resistance_from_conductance, series_resistance,
308        voltage,
309    };
310
311    const EPSILON: f64 = 1.0e-12;
312
313    fn assert_some_close(actual: Option<f64>, expected: f64) {
314        let actual = actual.expect("expected Some value");
315        let tolerance = expected.abs().max(1.0) * EPSILON;
316        assert!(
317            (actual - expected).abs() <= tolerance,
318            "expected {expected}, got {actual}"
319        );
320    }
321
322    #[test]
323    fn charge_helpers_cover_common_relationships() {
324        assert_eq!(charge_from_current_time(2.0, 3.0), Some(6.0));
325        assert_eq!(charge_from_current_time(2.0, -1.0), None);
326        assert_eq!(current_from_charge_time(10.0, 2.0), Some(5.0));
327        assert_eq!(current_from_charge_time(10.0, 0.0), None);
328    }
329
330    #[test]
331    fn ohms_law_helpers_cover_common_relationships() {
332        assert_eq!(voltage(2.0, 5.0), Some(10.0));
333        assert_eq!(voltage(2.0, -5.0), None);
334        assert_eq!(current(10.0, 5.0), Some(2.0));
335        assert_eq!(current(10.0, 0.0), None);
336        assert_eq!(resistance(10.0, 2.0), Some(5.0));
337        assert_eq!(resistance(10.0, 0.0), None);
338        assert_eq!(resistance(-10.0, 2.0), None);
339    }
340
341    #[test]
342    fn conductance_helpers_cover_common_relationships() {
343        assert_some_close(conductance(5.0), 0.2);
344        assert_eq!(conductance(0.0), None);
345        assert_some_close(resistance_from_conductance(0.2), 5.0);
346        assert_eq!(resistance_from_conductance(0.0), None);
347    }
348
349    #[test]
350    fn power_helpers_cover_common_relationships() {
351        assert_eq!(power_from_voltage_current(10.0, 2.0), Some(20.0));
352        assert_eq!(power_from_current_resistance(2.0, 5.0), Some(20.0));
353        assert_eq!(power_from_voltage_resistance(10.0, 5.0), Some(20.0));
354    }
355
356    #[test]
357    fn energy_helpers_cover_common_relationships() {
358        assert_eq!(energy_from_power_time(20.0, 3.0), Some(60.0));
359        assert_eq!(energy_from_power_time(20.0, -1.0), None);
360        assert_eq!(energy_from_voltage_charge(10.0, 2.0), Some(20.0));
361    }
362
363    #[test]
364    fn resistance_network_helpers_cover_common_relationships() {
365        assert_eq!(series_resistance(&[1.0, 2.0, 3.0]), Some(6.0));
366        assert_eq!(series_resistance(&[]), Some(0.0));
367        assert_eq!(series_resistance(&[1.0, -2.0]), None);
368
369        assert_eq!(parallel_resistance(&[2.0, 2.0]), Some(1.0));
370        assert_eq!(parallel_resistance(&[]), None);
371        assert_eq!(parallel_resistance(&[2.0, 0.0]), None);
372    }
373
374    #[test]
375    fn coulomb_force_helpers_cover_common_relationships() {
376        assert_some_close(coulomb_force(1.0, 1.0, 1.0), COULOMB_CONSTANT);
377        assert_some_close(coulomb_force(1.0, -1.0, 1.0), -COULOMB_CONSTANT);
378        assert_eq!(coulomb_force(1.0, 1.0, 0.0), None);
379    }
380
381    #[test]
382    fn electrical_load_delegates_to_public_helpers() {
383        let load = ElectricalLoad::new(10.0, 5.0).expect("valid load");
384
385        assert_eq!(load.current(), Some(2.0));
386        assert_eq!(load.power(), Some(20.0));
387        assert_eq!(ElectricalLoad::new(10.0, 0.0), None);
388    }
389
390    #[test]
391    fn non_finite_values_are_rejected() {
392        assert_eq!(charge_from_current_time(f64::NAN, 1.0), None);
393        assert_eq!(current_from_charge_time(1.0, f64::INFINITY), None);
394        assert_eq!(power_from_voltage_current(f64::INFINITY, 1.0), None);
395        assert_eq!(series_resistance(&[1.0, f64::NAN]), None);
396    }
397}