1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub mod prelude;
7
8fn finite(value: f64) -> Option<f64> {
9 value.is_finite().then_some(value)
10}
11
12#[must_use]
28pub fn work(force: f64, displacement: f64) -> Option<f64> {
29 if !force.is_finite() || !displacement.is_finite() {
30 return None;
31 }
32
33 finite(force * displacement)
34}
35
36#[must_use]
53pub fn work_at_angle(force: f64, displacement: f64, angle_radians: f64) -> Option<f64> {
54 if !force.is_finite() || !displacement.is_finite() || !angle_radians.is_finite() {
55 return None;
56 }
57
58 finite(force * displacement * angle_radians.cos())
59}
60
61#[must_use]
66pub fn work_at_angle_degrees(force: f64, displacement: f64, angle_degrees: f64) -> Option<f64> {
67 work_at_angle(force, displacement, angle_degrees.to_radians())
68}
69
70#[must_use]
77pub fn force_from_work(work: f64, displacement: f64) -> Option<f64> {
78 if !work.is_finite() || !displacement.is_finite() || displacement == 0.0 {
79 return None;
80 }
81
82 finite(work / displacement)
83}
84
85#[must_use]
92pub fn displacement_from_work(work: f64, force: f64) -> Option<f64> {
93 if !work.is_finite() || !force.is_finite() || force == 0.0 {
94 return None;
95 }
96
97 finite(work / force)
98}
99
100#[must_use]
114pub fn net_work(works: &[f64]) -> Option<f64> {
115 let mut total = 0.0;
116
117 for &value in works {
118 if !value.is_finite() {
119 return None;
120 }
121
122 total += value;
123
124 if !total.is_finite() {
125 return None;
126 }
127 }
128
129 Some(total)
130}
131
132#[must_use]
139pub fn work_from_force_samples(displacements: &[f64], forces: &[f64]) -> Option<f64> {
140 if displacements.len() != forces.len() {
141 return None;
142 }
143
144 let mut total = 0.0;
145
146 for (&displacement, &force) in displacements.iter().zip(forces.iter()) {
147 if !displacement.is_finite() || !force.is_finite() {
148 return None;
149 }
150
151 total += force * displacement;
152
153 if !total.is_finite() {
154 return None;
155 }
156 }
157
158 Some(total)
159}
160
161#[must_use]
177pub fn work_from_kinetic_energy_change(
178 initial_kinetic_energy: f64,
179 final_kinetic_energy: f64,
180) -> Option<f64> {
181 if !initial_kinetic_energy.is_finite()
182 || !final_kinetic_energy.is_finite()
183 || initial_kinetic_energy < 0.0
184 || final_kinetic_energy < 0.0
185 {
186 return None;
187 }
188
189 finite(final_kinetic_energy - initial_kinetic_energy)
190}
191
192#[must_use]
199pub fn final_kinetic_energy_from_work(initial_kinetic_energy: f64, work: f64) -> Option<f64> {
200 if !initial_kinetic_energy.is_finite() || !work.is_finite() || initial_kinetic_energy < 0.0 {
201 return None;
202 }
203
204 let result = initial_kinetic_energy + work;
205
206 if result < 0.0 {
207 return None;
208 }
209
210 finite(result)
211}
212
213#[must_use]
220pub fn initial_kinetic_energy_from_work(final_kinetic_energy: f64, work: f64) -> Option<f64> {
221 if !final_kinetic_energy.is_finite() || !work.is_finite() || final_kinetic_energy < 0.0 {
222 return None;
223 }
224
225 let result = final_kinetic_energy - work;
226
227 if result < 0.0 {
228 return None;
229 }
230
231 finite(result)
232}
233
234#[must_use]
250pub fn spring_work(
251 spring_constant: f64,
252 initial_displacement: f64,
253 final_displacement: f64,
254) -> Option<f64> {
255 if !spring_constant.is_finite()
256 || !initial_displacement.is_finite()
257 || !final_displacement.is_finite()
258 || spring_constant < 0.0
259 {
260 return None;
261 }
262
263 let initial_squared = initial_displacement * initial_displacement;
264 let final_squared = final_displacement * final_displacement;
265
266 finite(0.5 * spring_constant * (initial_squared - final_squared))
267}
268
269#[must_use]
276pub fn spring_potential_energy(spring_constant: f64, displacement: f64) -> Option<f64> {
277 if !spring_constant.is_finite() || !displacement.is_finite() || spring_constant < 0.0 {
278 return None;
279 }
280
281 finite(0.5 * spring_constant * displacement.powi(2))
282}
283
284#[must_use]
301pub fn work_against_gravity(
302 mass: f64,
303 gravitational_acceleration: f64,
304 height: f64,
305) -> Option<f64> {
306 if !mass.is_finite()
307 || !gravitational_acceleration.is_finite()
308 || !height.is_finite()
309 || mass < 0.0
310 {
311 return None;
312 }
313
314 finite(mass * gravitational_acceleration * height)
315}
316
317#[must_use]
324pub fn work_by_gravity(
325 mass: f64,
326 gravitational_acceleration: f64,
327 height_change: f64,
328) -> Option<f64> {
329 if !mass.is_finite()
330 || !gravitational_acceleration.is_finite()
331 || !height_change.is_finite()
332 || mass < 0.0
333 {
334 return None;
335 }
336
337 finite(-mass * gravitational_acceleration * height_change)
338}
339
340#[must_use]
347pub fn work_by_friction(friction_force_magnitude: f64, displacement: f64) -> Option<f64> {
348 if !friction_force_magnitude.is_finite()
349 || !displacement.is_finite()
350 || friction_force_magnitude < 0.0
351 {
352 return None;
353 }
354
355 finite(-friction_force_magnitude * displacement.abs())
356}
357
358#[derive(Debug, Clone, Copy, PartialEq)]
360pub struct ConstantForceWork {
361 pub force: f64,
362 pub displacement: f64,
363}
364
365impl ConstantForceWork {
366 #[must_use]
370 pub const fn new(force: f64, displacement: f64) -> Option<Self> {
371 if !force.is_finite() || !displacement.is_finite() {
372 return None;
373 }
374
375 Some(Self {
376 force,
377 displacement,
378 })
379 }
380
381 #[must_use]
393 pub fn work(&self) -> Option<f64> {
394 work(self.force, self.displacement)
395 }
396
397 #[must_use]
399 pub fn work_at_angle(&self, angle_radians: f64) -> Option<f64> {
400 work_at_angle(self.force, self.displacement, angle_radians)
401 }
402}
403
404#[cfg(test)]
405#[allow(clippy::float_cmp)]
406mod tests {
407 use super::{
408 ConstantForceWork, displacement_from_work, final_kinetic_energy_from_work, force_from_work,
409 initial_kinetic_energy_from_work, net_work, spring_potential_energy, spring_work, work,
410 work_against_gravity, work_at_angle, work_at_angle_degrees, work_by_friction,
411 work_by_gravity, work_from_force_samples, work_from_kinetic_energy_change,
412 };
413
414 fn approx_eq(left: f64, right: f64, tolerance: f64) {
415 let delta = (left - right).abs();
416
417 assert!(
418 delta <= tolerance,
419 "left={left} right={right} delta={delta} tolerance={tolerance}"
420 );
421 }
422
423 #[test]
424 fn work_handles_basic_cases() {
425 assert_eq!(work(10.0, 2.0), Some(20.0));
426 assert_eq!(work(-10.0, 2.0), Some(-20.0));
427 assert_eq!(work(10.0, -2.0), Some(-20.0));
428 }
429
430 #[test]
431 fn angled_work_handles_radians_and_degrees() {
432 assert_eq!(work_at_angle(10.0, 2.0, 0.0), Some(20.0));
433 approx_eq(work_at_angle_degrees(10.0, 2.0, 60.0).unwrap(), 10.0, 1e-12);
434 approx_eq(work_at_angle_degrees(10.0, 2.0, 90.0).unwrap(), 0.0, 1e-10);
435 }
436
437 #[test]
438 fn inverse_helpers_require_non_zero_divisors() {
439 assert_eq!(force_from_work(20.0, 2.0), Some(10.0));
440 assert_eq!(force_from_work(20.0, 0.0), None);
441 assert_eq!(displacement_from_work(20.0, 10.0), Some(2.0));
442 assert_eq!(displacement_from_work(20.0, 0.0), None);
443 }
444
445 #[test]
446 fn net_work_and_force_samples_cover_common_cases() {
447 assert_eq!(net_work(&[10.0, -2.0, 5.0]), Some(13.0));
448 assert_eq!(net_work(&[]), Some(0.0));
449 assert_eq!(
450 work_from_force_samples(&[1.0, 2.0, 3.0], &[10.0, 20.0, 30.0]),
451 Some(140.0)
452 );
453 assert_eq!(work_from_force_samples(&[1.0], &[10.0, 20.0]), None);
454 }
455
456 #[test]
457 fn work_energy_relationships_cover_forward_and_inverse_paths() {
458 assert_eq!(work_from_kinetic_energy_change(5.0, 12.0), Some(7.0));
459 assert_eq!(work_from_kinetic_energy_change(12.0, 5.0), Some(-7.0));
460 assert_eq!(work_from_kinetic_energy_change(-1.0, 5.0), None);
461
462 assert_eq!(final_kinetic_energy_from_work(5.0, 7.0), Some(12.0));
463 assert_eq!(final_kinetic_energy_from_work(5.0, -10.0), None);
464
465 assert_eq!(initial_kinetic_energy_from_work(12.0, 7.0), Some(5.0));
466 assert_eq!(initial_kinetic_energy_from_work(5.0, 10.0), None);
467 }
468
469 #[test]
470 fn spring_helpers_cover_energy_and_work() {
471 assert_eq!(spring_potential_energy(100.0, 0.5), Some(12.5));
472 assert_eq!(spring_work(100.0, 0.5, 0.0), Some(12.5));
473 assert_eq!(spring_work(100.0, 0.0, 0.5), Some(-12.5));
474 assert_eq!(spring_work(-100.0, 0.5, 0.0), None);
475 }
476
477 #[test]
478 fn gravity_and_friction_helpers_cover_common_cases() {
479 approx_eq(
480 work_against_gravity(2.0, 9.806_65, 10.0).unwrap(),
481 196.133,
482 1e-12,
483 );
484 approx_eq(
485 work_by_gravity(2.0, 9.806_65, 10.0).unwrap(),
486 -196.133,
487 1e-12,
488 );
489
490 assert_eq!(work_by_friction(5.0, 10.0), Some(-50.0));
491 assert_eq!(work_by_friction(5.0, -10.0), Some(-50.0));
492 assert_eq!(work_by_friction(-5.0, 10.0), None);
493 }
494
495 #[test]
496 fn constant_force_work_requires_finite_inputs() {
497 assert_eq!(
498 ConstantForceWork::new(10.0, 2.0).unwrap().work(),
499 Some(20.0)
500 );
501 assert_eq!(ConstantForceWork::new(f64::NAN, 2.0), None);
502 }
503}