Skip to main content

use_momentum/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Linear momentum, impulse, and recoil helpers.
5
6pub mod prelude;
7
8/// A moving mass with scalar velocity.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct MovingMass {
11    pub mass: f64,
12    pub velocity: f64,
13}
14
15impl MovingMass {
16    /// Creates a moving mass when `mass` is non-negative and both values are finite.
17    #[must_use]
18    pub fn new(mass: f64, velocity: f64) -> Option<Self> {
19        if !is_nonnegative_finite(mass) || !velocity.is_finite() {
20            return None;
21        }
22
23        Some(Self { mass, velocity })
24    }
25
26    /// Computes linear momentum using `p = m * v`.
27    ///
28    /// # Examples
29    ///
30    /// ```rust
31    /// use use_momentum::MovingMass;
32    ///
33    /// let moving_mass = MovingMass::new(2.0, 3.0).unwrap();
34    ///
35    /// assert_eq!(moving_mass.momentum(), Some(6.0));
36    /// ```
37    #[must_use]
38    pub fn momentum(&self) -> Option<f64> {
39        momentum(self.mass, self.velocity)
40    }
41
42    /// Computes kinetic energy using `0.5 * m * v^2`.
43    #[must_use]
44    pub fn kinetic_energy(&self) -> Option<f64> {
45        finite_result(0.5 * self.mass * self.velocity * self.velocity)
46    }
47}
48
49/// Computes linear momentum using `p = m * v`.
50///
51/// Returns `None` when `mass` is negative, when either input is not finite, or when the
52/// computed momentum is not finite.
53///
54/// # Examples
55///
56/// ```rust
57/// use use_momentum::momentum;
58///
59/// assert_eq!(momentum(2.0, 3.0), Some(6.0));
60/// assert_eq!(momentum(2.0, -3.0), Some(-6.0));
61/// ```
62#[must_use]
63pub fn momentum(mass: f64, velocity: f64) -> Option<f64> {
64    if !is_nonnegative_finite(mass) || !velocity.is_finite() {
65        return None;
66    }
67
68    finite_result(mass * velocity)
69}
70
71/// Computes velocity from momentum and mass using `v = p / m`.
72///
73/// Returns `None` when `mass` is less than or equal to zero, when either input is not finite,
74/// or when the computed velocity is not finite.
75#[must_use]
76pub fn velocity_from_momentum(momentum: f64, mass: f64) -> Option<f64> {
77    if !momentum.is_finite() || !is_positive_finite(mass) {
78        return None;
79    }
80
81    finite_result(momentum / mass)
82}
83
84/// Computes mass from momentum and velocity using `m = p / v`.
85///
86/// Returns `None` when `velocity` is zero, when either input is not finite, when the computed
87/// mass is negative, or when the computed mass is not finite.
88#[must_use]
89pub fn mass_from_momentum(momentum: f64, velocity: f64) -> Option<f64> {
90    if !momentum.is_finite() || !velocity.is_finite() || velocity == 0.0 {
91        return None;
92    }
93
94    let mass = momentum / velocity;
95    if mass < 0.0 {
96        return None;
97    }
98
99    finite_result(mass)
100}
101
102/// Computes impulse from force and elapsed time using `J = F * Δt`.
103///
104/// Returns `None` when `time` is negative, when either input is not finite, or when the
105/// computed impulse is not finite.
106///
107/// # Examples
108///
109/// ```rust
110/// use use_momentum::impulse;
111///
112/// assert_eq!(impulse(10.0, 2.0), Some(20.0));
113/// assert_eq!(impulse(-10.0, 2.0), Some(-20.0));
114/// ```
115#[must_use]
116pub fn impulse(force: f64, time: f64) -> Option<f64> {
117    if !force.is_finite() || !time.is_finite() || time < 0.0 {
118        return None;
119    }
120
121    finite_result(force * time)
122}
123
124/// Computes impulse from a change in momentum using `J = p_final - p_initial`.
125///
126/// Returns `None` when either input is not finite or when the computed impulse is not finite.
127#[must_use]
128pub fn impulse_from_momentum_change(initial_momentum: f64, final_momentum: f64) -> Option<f64> {
129    if !initial_momentum.is_finite() || !final_momentum.is_finite() {
130        return None;
131    }
132
133    finite_result(final_momentum - initial_momentum)
134}
135
136/// Computes average force from impulse and elapsed time using `F = J / Δt`.
137///
138/// Returns `None` when `time` is less than or equal to zero, when either input is not finite,
139/// or when the computed force is not finite.
140#[must_use]
141pub fn average_force_from_impulse(impulse: f64, time: f64) -> Option<f64> {
142    if !impulse.is_finite() || !is_positive_finite(time) {
143        return None;
144    }
145
146    finite_result(impulse / time)
147}
148
149/// Computes the total momentum of a slice of momentum values.
150///
151/// Returns `Some(0.0)` for an empty slice. Returns `None` when any momentum value is not finite
152/// or when the sum is not finite.
153#[must_use]
154pub fn total_momentum(momenta: &[f64]) -> Option<f64> {
155    momenta.iter().try_fold(0.0, |sum, momentum| {
156        if !momentum.is_finite() {
157            return None;
158        }
159
160        finite_result(sum + *momentum)
161    })
162}
163
164/// Computes the total momentum of two moving bodies using `p_total = m1v1 + m2v2`.
165///
166/// Returns `None` when either mass is negative, when any input is not finite, or when the total
167/// momentum is not finite.
168#[must_use]
169pub fn two_body_total_momentum(
170    mass_a: f64,
171    velocity_a: f64,
172    mass_b: f64,
173    velocity_b: f64,
174) -> Option<f64> {
175    let momentum_a = momentum(mass_a, velocity_a)?;
176    let momentum_b = momentum(mass_b, velocity_b)?;
177
178    finite_result(momentum_a + momentum_b)
179}
180
181/// Computes recoil velocity assuming the initial total momentum is zero.
182///
183/// Uses `v_recoil = -(projectile_mass * projectile_velocity) / body_mass`.
184///
185/// Returns `None` when `projectile_mass` is negative, when `body_mass` is less than or equal to
186/// zero, when any input is not finite, or when the computed recoil velocity is not finite.
187///
188/// # Examples
189///
190/// ```rust
191/// use use_momentum::recoil_velocity;
192///
193/// assert_eq!(recoil_velocity(1.0, 10.0, 5.0), Some(-2.0));
194/// ```
195#[must_use]
196pub fn recoil_velocity(
197    projectile_mass: f64,
198    projectile_velocity: f64,
199    body_mass: f64,
200) -> Option<f64> {
201    if !is_nonnegative_finite(projectile_mass)
202        || !projectile_velocity.is_finite()
203        || !is_positive_finite(body_mass)
204    {
205        return None;
206    }
207
208    let projectile_momentum = momentum(projectile_mass, projectile_velocity)?;
209    finite_result(-(projectile_momentum / body_mass))
210}
211
212fn finite_result(value: f64) -> Option<f64> {
213    value.is_finite().then_some(value)
214}
215
216fn is_nonnegative_finite(value: f64) -> bool {
217    value.is_finite() && value >= 0.0
218}
219
220fn is_positive_finite(value: f64) -> bool {
221    value.is_finite() && value > 0.0
222}
223
224#[cfg(test)]
225#[allow(clippy::float_cmp)]
226mod tests {
227    use super::{
228        MovingMass, average_force_from_impulse, impulse, impulse_from_momentum_change,
229        mass_from_momentum, momentum, recoil_velocity, total_momentum, two_body_total_momentum,
230        velocity_from_momentum,
231    };
232
233    #[test]
234    fn momentum_helpers_cover_common_cases() {
235        assert_eq!(momentum(2.0, 3.0), Some(6.0));
236        assert_eq!(momentum(2.0, -3.0), Some(-6.0));
237        assert_eq!(momentum(-1.0, 3.0), None);
238
239        assert_eq!(velocity_from_momentum(10.0, 2.0), Some(5.0));
240        assert_eq!(velocity_from_momentum(10.0, 0.0), None);
241
242        assert_eq!(mass_from_momentum(10.0, 2.0), Some(5.0));
243        assert_eq!(mass_from_momentum(10.0, 0.0), None);
244        assert_eq!(mass_from_momentum(-10.0, 2.0), None);
245    }
246
247    #[test]
248    fn impulse_helpers_cover_common_cases() {
249        assert_eq!(impulse(10.0, 2.0), Some(20.0));
250        assert_eq!(impulse(-10.0, 2.0), Some(-20.0));
251        assert_eq!(impulse(10.0, -1.0), None);
252
253        assert_eq!(impulse_from_momentum_change(5.0, 12.0), Some(7.0));
254        assert_eq!(average_force_from_impulse(20.0, 4.0), Some(5.0));
255        assert_eq!(average_force_from_impulse(20.0, 0.0), None);
256    }
257
258    #[test]
259    fn conservation_helpers_cover_common_cases() {
260        assert_eq!(total_momentum(&[1.0, 2.0, 3.0]), Some(6.0));
261        assert_eq!(total_momentum(&[]), Some(0.0));
262        assert_eq!(two_body_total_momentum(2.0, 3.0, 4.0, -1.0), Some(2.0));
263    }
264
265    #[test]
266    fn recoil_and_moving_mass_cover_common_cases() {
267        assert_eq!(recoil_velocity(1.0, 10.0, 5.0), Some(-2.0));
268        assert_eq!(MovingMass::new(2.0, 3.0).unwrap().momentum(), Some(6.0));
269        assert_eq!(MovingMass::new(-1.0, 3.0), None);
270    }
271
272    #[test]
273    fn non_finite_inputs_are_rejected() {
274        assert_eq!(momentum(f64::INFINITY, 1.0), None);
275        assert_eq!(velocity_from_momentum(1.0, f64::NAN), None);
276        assert_eq!(impulse(f64::NAN, 1.0), None);
277        assert_eq!(total_momentum(&[1.0, f64::INFINITY]), None);
278        assert_eq!(recoil_velocity(1.0, 10.0, f64::INFINITY), None);
279    }
280
281    #[test]
282    fn moving_mass_computes_kinetic_energy() {
283        assert_eq!(
284            MovingMass::new(2.0, 3.0).unwrap().kinetic_energy(),
285            Some(9.0)
286        );
287    }
288}