1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub mod prelude;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct MovingMass {
11 pub mass: f64,
12 pub velocity: f64,
13}
14
15impl MovingMass {
16 #[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 #[must_use]
38 pub fn momentum(&self) -> Option<f64> {
39 momentum(self.mass, self.velocity)
40 }
41
42 #[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}