1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub mod prelude;
7
8pub 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq)]
260pub struct ElectricalLoad {
261 pub voltage: f64,
262 pub resistance: f64,
263}
264
265impl ElectricalLoad {
266 #[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 #[must_use]
281 pub fn current(&self) -> Option<f64> {
282 current(self.voltage, self.resistance)
283 }
284
285 #[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}