#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
pub mod prelude;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MovingMass {
pub mass: f64,
pub velocity: f64,
}
impl MovingMass {
#[must_use]
pub fn new(mass: f64, velocity: f64) -> Option<Self> {
if !is_nonnegative_finite(mass) || !velocity.is_finite() {
return None;
}
Some(Self { mass, velocity })
}
#[must_use]
pub fn momentum(&self) -> Option<f64> {
momentum(self.mass, self.velocity)
}
#[must_use]
pub fn kinetic_energy(&self) -> Option<f64> {
finite_result(0.5 * self.mass * self.velocity * self.velocity)
}
}
#[must_use]
pub fn momentum(mass: f64, velocity: f64) -> Option<f64> {
if !is_nonnegative_finite(mass) || !velocity.is_finite() {
return None;
}
finite_result(mass * velocity)
}
#[must_use]
pub fn velocity_from_momentum(momentum: f64, mass: f64) -> Option<f64> {
if !momentum.is_finite() || !is_positive_finite(mass) {
return None;
}
finite_result(momentum / mass)
}
#[must_use]
pub fn mass_from_momentum(momentum: f64, velocity: f64) -> Option<f64> {
if !momentum.is_finite() || !velocity.is_finite() || velocity == 0.0 {
return None;
}
let mass = momentum / velocity;
if mass < 0.0 {
return None;
}
finite_result(mass)
}
#[must_use]
pub fn impulse(force: f64, time: f64) -> Option<f64> {
if !force.is_finite() || !time.is_finite() || time < 0.0 {
return None;
}
finite_result(force * time)
}
#[must_use]
pub fn impulse_from_momentum_change(initial_momentum: f64, final_momentum: f64) -> Option<f64> {
if !initial_momentum.is_finite() || !final_momentum.is_finite() {
return None;
}
finite_result(final_momentum - initial_momentum)
}
#[must_use]
pub fn average_force_from_impulse(impulse: f64, time: f64) -> Option<f64> {
if !impulse.is_finite() || !is_positive_finite(time) {
return None;
}
finite_result(impulse / time)
}
#[must_use]
pub fn total_momentum(momenta: &[f64]) -> Option<f64> {
momenta.iter().try_fold(0.0, |sum, momentum| {
if !momentum.is_finite() {
return None;
}
finite_result(sum + *momentum)
})
}
#[must_use]
pub fn two_body_total_momentum(
mass_a: f64,
velocity_a: f64,
mass_b: f64,
velocity_b: f64,
) -> Option<f64> {
let momentum_a = momentum(mass_a, velocity_a)?;
let momentum_b = momentum(mass_b, velocity_b)?;
finite_result(momentum_a + momentum_b)
}
#[must_use]
pub fn recoil_velocity(
projectile_mass: f64,
projectile_velocity: f64,
body_mass: f64,
) -> Option<f64> {
if !is_nonnegative_finite(projectile_mass)
|| !projectile_velocity.is_finite()
|| !is_positive_finite(body_mass)
{
return None;
}
let projectile_momentum = momentum(projectile_mass, projectile_velocity)?;
finite_result(-(projectile_momentum / body_mass))
}
fn finite_result(value: f64) -> Option<f64> {
value.is_finite().then_some(value)
}
fn is_nonnegative_finite(value: f64) -> bool {
value.is_finite() && value >= 0.0
}
fn is_positive_finite(value: f64) -> bool {
value.is_finite() && value > 0.0
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::{
MovingMass, average_force_from_impulse, impulse, impulse_from_momentum_change,
mass_from_momentum, momentum, recoil_velocity, total_momentum, two_body_total_momentum,
velocity_from_momentum,
};
#[test]
fn momentum_helpers_cover_common_cases() {
assert_eq!(momentum(2.0, 3.0), Some(6.0));
assert_eq!(momentum(2.0, -3.0), Some(-6.0));
assert_eq!(momentum(-1.0, 3.0), None);
assert_eq!(velocity_from_momentum(10.0, 2.0), Some(5.0));
assert_eq!(velocity_from_momentum(10.0, 0.0), None);
assert_eq!(mass_from_momentum(10.0, 2.0), Some(5.0));
assert_eq!(mass_from_momentum(10.0, 0.0), None);
assert_eq!(mass_from_momentum(-10.0, 2.0), None);
}
#[test]
fn impulse_helpers_cover_common_cases() {
assert_eq!(impulse(10.0, 2.0), Some(20.0));
assert_eq!(impulse(-10.0, 2.0), Some(-20.0));
assert_eq!(impulse(10.0, -1.0), None);
assert_eq!(impulse_from_momentum_change(5.0, 12.0), Some(7.0));
assert_eq!(average_force_from_impulse(20.0, 4.0), Some(5.0));
assert_eq!(average_force_from_impulse(20.0, 0.0), None);
}
#[test]
fn conservation_helpers_cover_common_cases() {
assert_eq!(total_momentum(&[1.0, 2.0, 3.0]), Some(6.0));
assert_eq!(total_momentum(&[]), Some(0.0));
assert_eq!(two_body_total_momentum(2.0, 3.0, 4.0, -1.0), Some(2.0));
}
#[test]
fn recoil_and_moving_mass_cover_common_cases() {
assert_eq!(recoil_velocity(1.0, 10.0, 5.0), Some(-2.0));
assert_eq!(MovingMass::new(2.0, 3.0).unwrap().momentum(), Some(6.0));
assert_eq!(MovingMass::new(-1.0, 3.0), None);
}
#[test]
fn non_finite_inputs_are_rejected() {
assert_eq!(momentum(f64::INFINITY, 1.0), None);
assert_eq!(velocity_from_momentum(1.0, f64::NAN), None);
assert_eq!(impulse(f64::NAN, 1.0), None);
assert_eq!(total_momentum(&[1.0, f64::INFINITY]), None);
assert_eq!(recoil_velocity(1.0, 10.0, f64::INFINITY), None);
}
#[test]
fn moving_mass_computes_kinetic_energy() {
assert_eq!(
MovingMass::new(2.0, 3.0).unwrap().kinetic_energy(),
Some(9.0)
);
}
}