Skip to main content

use_rotation/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::f64::consts::TAU;
5
6pub mod prelude;
7
8/// A rotating body with scalar moment of inertia and angular velocity.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct RotatingBody {
11    pub moment_of_inertia: f64,
12    pub angular_velocity: f64,
13}
14
15impl RotatingBody {
16    /// Creates a rotating body when `moment_of_inertia` is non-negative and both values are finite.
17    #[must_use]
18    pub const fn new(moment_of_inertia: f64, angular_velocity: f64) -> Option<Self> {
19        if !moment_of_inertia.is_finite()
20            || moment_of_inertia < 0.0
21            || !angular_velocity.is_finite()
22        {
23            return None;
24        }
25
26        Some(Self {
27            moment_of_inertia,
28            angular_velocity,
29        })
30    }
31
32    /// Computes angular momentum using `L = Iω`.
33    #[must_use]
34    pub fn angular_momentum(&self) -> Option<f64> {
35        angular_momentum(self.moment_of_inertia, self.angular_velocity)
36    }
37
38    /// Computes rotational kinetic energy using `KE_rot = 0.5 * I * ω²`.
39    ///
40    /// # Examples
41    ///
42    /// ```rust
43    /// use use_rotation::RotatingBody;
44    ///
45    /// let body = RotatingBody::new(4.0, 5.0).unwrap();
46    ///
47    /// assert_eq!(body.rotational_kinetic_energy(), Some(50.0));
48    /// ```
49    #[must_use]
50    pub fn rotational_kinetic_energy(&self) -> Option<f64> {
51        rotational_kinetic_energy(self.moment_of_inertia, self.angular_velocity)
52    }
53
54    /// Computes angular acceleration from applied torque using `α = τ / I`.
55    #[must_use]
56    pub fn angular_acceleration_from_torque(&self, torque: f64) -> Option<f64> {
57        angular_acceleration_from_torque(torque, self.moment_of_inertia)
58    }
59}
60
61/// A scalar angular position and angular velocity pair.
62#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct AngularState {
64    pub angular_position: f64,
65    pub angular_velocity: f64,
66}
67
68impl AngularState {
69    /// Creates an angular state when both values are finite.
70    #[must_use]
71    pub const fn new(angular_position: f64, angular_velocity: f64) -> Option<Self> {
72        if !angular_position.is_finite() || !angular_velocity.is_finite() {
73            return None;
74        }
75
76        Some(Self {
77            angular_position,
78            angular_velocity,
79        })
80    }
81
82    /// Advances the state under constant angular acceleration.
83    ///
84    /// # Examples
85    ///
86    /// ```rust
87    /// use use_rotation::AngularState;
88    ///
89    /// let next = AngularState::new(1.0, 2.0)
90    ///     .unwrap()
91    ///     .advanced_by_constant_acceleration(3.0, 4.0)
92    ///     .unwrap();
93    ///
94    /// assert_eq!(
95    ///     next,
96    ///     AngularState {
97    ///         angular_position: 33.0,
98    ///         angular_velocity: 14.0,
99    ///     }
100    /// );
101    /// ```
102    #[must_use]
103    pub fn advanced_by_constant_acceleration(
104        &self,
105        angular_acceleration: f64,
106        time: f64,
107    ) -> Option<Self> {
108        let displacement = angular_displacement(self.angular_velocity, angular_acceleration, time)?;
109        let angular_velocity =
110            final_angular_velocity(self.angular_velocity, angular_acceleration, time)?;
111        let angular_position = finite_result(self.angular_position + displacement)?;
112
113        Some(Self {
114            angular_position,
115            angular_velocity,
116        })
117    }
118}
119
120/// Converts degrees to radians.
121///
122/// Returns `None` when the input or result is not finite.
123#[must_use]
124pub fn radians_from_degrees(degrees: f64) -> Option<f64> {
125    if !degrees.is_finite() {
126        return None;
127    }
128
129    finite_result(degrees.to_radians())
130}
131
132/// Converts radians to degrees.
133///
134/// Returns `None` when the input or result is not finite.
135#[must_use]
136pub fn degrees_from_radians(radians: f64) -> Option<f64> {
137    if !radians.is_finite() {
138        return None;
139    }
140
141    finite_result(radians.to_degrees())
142}
143
144/// Converts radians to revolutions.
145///
146/// Returns `None` when the input or result is not finite.
147#[must_use]
148pub fn revolutions_from_radians(radians: f64) -> Option<f64> {
149    if !radians.is_finite() {
150        return None;
151    }
152
153    finite_result(radians / TAU)
154}
155
156/// Converts revolutions to radians.
157///
158/// Returns `None` when the input or result is not finite.
159#[must_use]
160pub fn radians_from_revolutions(revolutions: f64) -> Option<f64> {
161    if !revolutions.is_finite() {
162        return None;
163    }
164
165    finite_result(revolutions * TAU)
166}
167
168/// Computes angular velocity using `ω = Δθ / t`.
169///
170/// Returns `None` when `time` is less than or equal to zero, when any input is not finite, or
171/// when the computed angular velocity is not finite.
172///
173/// # Examples
174///
175/// ```rust
176/// use use_rotation::angular_velocity;
177///
178/// assert_eq!(angular_velocity(10.0, 2.0), Some(5.0));
179/// ```
180#[must_use]
181pub fn angular_velocity(angular_displacement: f64, time: f64) -> Option<f64> {
182    if !angular_displacement.is_finite() || !is_positive_finite(time) {
183        return None;
184    }
185
186    finite_result(angular_displacement / time)
187}
188
189/// Computes angular acceleration using `α = (ω_final - ω_initial) / t`.
190///
191/// Returns `None` when `time` is less than or equal to zero, when any input is not finite, or
192/// when the computed angular acceleration is not finite.
193///
194/// # Examples
195///
196/// ```rust
197/// use use_rotation::angular_acceleration;
198///
199/// assert_eq!(angular_acceleration(2.0, 10.0, 4.0), Some(2.0));
200/// ```
201#[must_use]
202pub fn angular_acceleration(
203    initial_angular_velocity: f64,
204    final_angular_velocity: f64,
205    time: f64,
206) -> Option<f64> {
207    if !initial_angular_velocity.is_finite()
208        || !final_angular_velocity.is_finite()
209        || !is_positive_finite(time)
210    {
211        return None;
212    }
213
214    finite_result((final_angular_velocity - initial_angular_velocity) / time)
215}
216
217/// Computes final angular velocity using `ω_final = ω_initial + αt`.
218///
219/// Returns `None` when `time` is negative, when any input is not finite, or when the computed
220/// angular velocity is not finite.
221#[must_use]
222pub fn final_angular_velocity(
223    initial_angular_velocity: f64,
224    angular_acceleration: f64,
225    time: f64,
226) -> Option<f64> {
227    if !initial_angular_velocity.is_finite()
228        || !angular_acceleration.is_finite()
229        || !is_nonnegative_finite(time)
230    {
231        return None;
232    }
233
234    finite_result(angular_acceleration.mul_add(time, initial_angular_velocity))
235}
236
237/// Computes angular displacement using `θ = ω_initial * t + 0.5 * α * t²`.
238///
239/// Returns `None` when `time` is negative, when any input is not finite, or when the computed
240/// angular displacement is not finite.
241#[must_use]
242pub fn angular_displacement(
243    initial_angular_velocity: f64,
244    angular_acceleration: f64,
245    time: f64,
246) -> Option<f64> {
247    if !initial_angular_velocity.is_finite()
248        || !angular_acceleration.is_finite()
249        || !is_nonnegative_finite(time)
250    {
251        return None;
252    }
253
254    let acceleration_term = 0.5 * angular_acceleration * time * time;
255
256    finite_result(initial_angular_velocity.mul_add(time, acceleration_term))
257}
258
259/// Computes squared final angular velocity using `ω_final² = ω_initial² + 2αθ`.
260///
261/// Returns `None` when any input is not finite, when the computed squared value is negative, or
262/// when the computed squared value is not finite.
263#[must_use]
264pub fn final_angular_velocity_squared(
265    initial_angular_velocity: f64,
266    angular_acceleration: f64,
267    angular_displacement: f64,
268) -> Option<f64> {
269    if !initial_angular_velocity.is_finite()
270        || !angular_acceleration.is_finite()
271        || !angular_displacement.is_finite()
272    {
273        return None;
274    }
275
276    let squared = initial_angular_velocity.mul_add(
277        initial_angular_velocity,
278        2.0 * angular_acceleration * angular_displacement,
279    );
280
281    if !squared.is_finite() || squared < 0.0 {
282        return None;
283    }
284
285    Some(squared)
286}
287
288/// Computes final angular velocity using `ω_final = sqrt(ω_initial² + 2αθ)`.
289///
290/// Returns `None` when the squared value is negative, when any input is not finite, or when the
291/// computed angular velocity is not finite.
292#[must_use]
293pub fn final_angular_velocity_from_displacement(
294    initial_angular_velocity: f64,
295    angular_acceleration: f64,
296    angular_displacement: f64,
297) -> Option<f64> {
298    let squared = final_angular_velocity_squared(
299        initial_angular_velocity,
300        angular_acceleration,
301        angular_displacement,
302    )?;
303
304    finite_result(squared.sqrt())
305}
306
307/// Computes tangential speed using `v = ωr`.
308///
309/// Returns `None` when `radius` is negative, when any input is not finite, or when the computed
310/// tangential speed is not finite.
311///
312/// # Examples
313///
314/// ```rust
315/// use use_rotation::tangential_speed;
316///
317/// assert_eq!(tangential_speed(3.0, 2.0), Some(6.0));
318/// ```
319#[must_use]
320pub fn tangential_speed(angular_velocity: f64, radius: f64) -> Option<f64> {
321    if !angular_velocity.is_finite() || !is_nonnegative_finite(radius) {
322        return None;
323    }
324
325    finite_result(angular_velocity * radius)
326}
327
328/// Computes angular velocity from tangential speed using `ω = v / r`.
329///
330/// Returns `None` when `radius` is less than or equal to zero, when any input is not finite, or
331/// when the computed angular velocity is not finite.
332#[must_use]
333pub fn angular_velocity_from_tangential_speed(tangential_speed: f64, radius: f64) -> Option<f64> {
334    if !tangential_speed.is_finite() || !is_positive_finite(radius) {
335        return None;
336    }
337
338    finite_result(tangential_speed / radius)
339}
340
341/// Computes tangential acceleration using `a_t = αr`.
342///
343/// Returns `None` when `radius` is negative, when any input is not finite, or when the computed
344/// tangential acceleration is not finite.
345#[must_use]
346pub fn tangential_acceleration(angular_acceleration: f64, radius: f64) -> Option<f64> {
347    if !angular_acceleration.is_finite() || !is_nonnegative_finite(radius) {
348        return None;
349    }
350
351    finite_result(angular_acceleration * radius)
352}
353
354/// Computes centripetal acceleration using `a_c = ω²r`.
355///
356/// Returns `None` when `radius` is negative, when any input is not finite, or when the computed
357/// acceleration is not finite.
358///
359/// # Examples
360///
361/// ```rust
362/// use use_rotation::centripetal_acceleration_from_angular_velocity;
363///
364/// assert_eq!(centripetal_acceleration_from_angular_velocity(3.0, 2.0), Some(18.0));
365/// ```
366#[must_use]
367pub fn centripetal_acceleration_from_angular_velocity(
368    angular_velocity: f64,
369    radius: f64,
370) -> Option<f64> {
371    if !angular_velocity.is_finite() || !is_nonnegative_finite(radius) {
372        return None;
373    }
374
375    let acceleration = angular_velocity * angular_velocity * radius;
376    if acceleration < 0.0 {
377        return None;
378    }
379
380    finite_result(acceleration)
381}
382
383/// Computes centripetal acceleration using `a_c = v² / r`.
384///
385/// Returns `None` when `radius` is less than or equal to zero, when any input is not finite, or
386/// when the computed acceleration is not finite.
387#[must_use]
388pub fn centripetal_acceleration_from_tangential_speed(
389    tangential_speed: f64,
390    radius: f64,
391) -> Option<f64> {
392    if !tangential_speed.is_finite() || !is_positive_finite(radius) {
393        return None;
394    }
395
396    let acceleration = tangential_speed * tangential_speed / radius;
397    if acceleration < 0.0 {
398        return None;
399    }
400
401    finite_result(acceleration)
402}
403
404/// Computes point-mass moment of inertia using `I = mr²`.
405///
406/// Returns `None` when `mass` or `radius` is negative, when any input is not finite, or when the
407/// computed moment of inertia is not finite.
408#[must_use]
409pub fn point_mass_moment_of_inertia(mass: f64, radius: f64) -> Option<f64> {
410    scaled_square_measure(mass, radius, 1.0)
411}
412
413/// Computes solid-disk moment of inertia using `I = 0.5mr²`.
414///
415/// Returns `None` when `mass` or `radius` is negative, when any input is not finite, or when the
416/// computed moment of inertia is not finite.
417///
418/// # Examples
419///
420/// ```rust
421/// use use_rotation::solid_disk_moment_of_inertia;
422///
423/// assert_eq!(solid_disk_moment_of_inertia(2.0, 3.0), Some(9.0));
424/// ```
425#[must_use]
426pub fn solid_disk_moment_of_inertia(mass: f64, radius: f64) -> Option<f64> {
427    scaled_square_measure(mass, radius, 0.5)
428}
429
430/// Computes thin-ring moment of inertia using `I = mr²`.
431///
432/// Returns `None` when `mass` or `radius` is negative, when any input is not finite, or when the
433/// computed moment of inertia is not finite.
434#[must_use]
435pub fn thin_ring_moment_of_inertia(mass: f64, radius: f64) -> Option<f64> {
436    point_mass_moment_of_inertia(mass, radius)
437}
438
439/// Computes solid-sphere moment of inertia using `I = (2 / 5)mr²`.
440///
441/// Returns `None` when `mass` or `radius` is negative, when any input is not finite, or when the
442/// computed moment of inertia is not finite.
443#[must_use]
444pub fn solid_sphere_moment_of_inertia(mass: f64, radius: f64) -> Option<f64> {
445    scaled_square_measure(mass, radius, 2.0 / 5.0)
446}
447
448/// Computes hollow-sphere moment of inertia using `I = (2 / 3)mr²`.
449///
450/// Returns `None` when `mass` or `radius` is negative, when any input is not finite, or when the
451/// computed moment of inertia is not finite.
452#[must_use]
453pub fn hollow_sphere_moment_of_inertia(mass: f64, radius: f64) -> Option<f64> {
454    scaled_square_measure(mass, radius, 2.0 / 3.0)
455}
456
457/// Computes rod moment of inertia about its center using `I = (1 / 12)mL²`.
458///
459/// Returns `None` when `mass` or `length` is negative, when any input is not finite, or when the
460/// computed moment of inertia is not finite.
461#[must_use]
462pub fn rod_moment_of_inertia_about_center(mass: f64, length: f64) -> Option<f64> {
463    scaled_square_measure(mass, length, 1.0 / 12.0)
464}
465
466/// Computes rod moment of inertia about one end using `I = (1 / 3)mL²`.
467///
468/// Returns `None` when `mass` or `length` is negative, when any input is not finite, or when the
469/// computed moment of inertia is not finite.
470#[must_use]
471pub fn rod_moment_of_inertia_about_end(mass: f64, length: f64) -> Option<f64> {
472    scaled_square_measure(mass, length, 1.0 / 3.0)
473}
474
475/// Computes angular momentum using `L = Iω`.
476///
477/// Returns `None` when `moment_of_inertia` is negative, when any input is not finite, or when the
478/// computed angular momentum is not finite.
479///
480/// # Examples
481///
482/// ```rust
483/// use use_rotation::angular_momentum;
484///
485/// assert_eq!(angular_momentum(4.0, 5.0), Some(20.0));
486/// ```
487#[must_use]
488pub fn angular_momentum(moment_of_inertia: f64, angular_velocity: f64) -> Option<f64> {
489    if !is_nonnegative_finite(moment_of_inertia) || !angular_velocity.is_finite() {
490        return None;
491    }
492
493    finite_result(moment_of_inertia * angular_velocity)
494}
495
496/// Computes angular velocity from angular momentum using `ω = L / I`.
497///
498/// Returns `None` when `moment_of_inertia` is less than or equal to zero, when any input is not
499/// finite, or when the computed angular velocity is not finite.
500#[must_use]
501pub fn angular_velocity_from_angular_momentum(
502    angular_momentum: f64,
503    moment_of_inertia: f64,
504) -> Option<f64> {
505    if !angular_momentum.is_finite() || !is_positive_finite(moment_of_inertia) {
506        return None;
507    }
508
509    finite_result(angular_momentum / moment_of_inertia)
510}
511
512/// Computes rotational kinetic energy using `KE_rot = 0.5 * I * ω²`.
513///
514/// Returns `None` when `moment_of_inertia` is negative, when any input is not finite, or when the
515/// computed kinetic energy is not finite.
516///
517/// # Examples
518///
519/// ```rust
520/// use use_rotation::rotational_kinetic_energy;
521///
522/// assert_eq!(rotational_kinetic_energy(4.0, 5.0), Some(50.0));
523/// ```
524#[must_use]
525pub fn rotational_kinetic_energy(moment_of_inertia: f64, angular_velocity: f64) -> Option<f64> {
526    if !is_nonnegative_finite(moment_of_inertia) || !angular_velocity.is_finite() {
527        return None;
528    }
529
530    let energy = 0.5 * moment_of_inertia * angular_velocity * angular_velocity;
531    if energy < 0.0 {
532        return None;
533    }
534
535    finite_result(energy)
536}
537
538/// Computes angular velocity from rotational kinetic energy using `ω = sqrt(2KE / I)`.
539///
540/// Returns the non-negative principal value when successful. Returns `None` when rotational
541/// kinetic energy is negative, when `moment_of_inertia` is less than or equal to zero, when any
542/// input is not finite, or when the computed angular velocity is not finite.
543#[must_use]
544pub fn angular_velocity_from_rotational_kinetic_energy(
545    rotational_kinetic_energy: f64,
546    moment_of_inertia: f64,
547) -> Option<f64> {
548    if !is_nonnegative_finite(rotational_kinetic_energy) || !is_positive_finite(moment_of_inertia) {
549        return None;
550    }
551
552    let squared = 2.0 * rotational_kinetic_energy / moment_of_inertia;
553    if !squared.is_finite() || squared < 0.0 {
554        return None;
555    }
556
557    finite_result(squared.sqrt())
558}
559
560/// Computes angular acceleration from torque using `α = τ / I`.
561///
562/// Returns `None` when `moment_of_inertia` is less than or equal to zero, when any input is not
563/// finite, or when the computed angular acceleration is not finite.
564///
565/// For broader torque-specific scalar helpers such as lever-arm and balancing relations, prefer
566/// `use-torque`.
567#[must_use]
568pub fn angular_acceleration_from_torque(torque: f64, moment_of_inertia: f64) -> Option<f64> {
569    if !torque.is_finite() || !is_positive_finite(moment_of_inertia) {
570        return None;
571    }
572
573    finite_result(torque / moment_of_inertia)
574}
575
576fn scaled_square_measure(primary: f64, measure: f64, factor: f64) -> Option<f64> {
577    if !is_nonnegative_finite(primary) || !is_nonnegative_finite(measure) {
578        return None;
579    }
580
581    finite_result(factor * primary * measure * measure)
582}
583
584fn is_nonnegative_finite(value: f64) -> bool {
585    value.is_finite() && value >= 0.0
586}
587
588fn is_positive_finite(value: f64) -> bool {
589    value.is_finite() && value > 0.0
590}
591
592fn finite_result(value: f64) -> Option<f64> {
593    value.is_finite().then_some(value)
594}
595
596#[cfg(test)]
597#[allow(clippy::float_cmp)]
598mod tests {
599    use super::{
600        AngularState, RotatingBody, angular_acceleration, angular_acceleration_from_torque,
601        angular_displacement, angular_momentum, angular_velocity,
602        angular_velocity_from_angular_momentum, angular_velocity_from_rotational_kinetic_energy,
603        angular_velocity_from_tangential_speed, centripetal_acceleration_from_angular_velocity,
604        centripetal_acceleration_from_tangential_speed, degrees_from_radians,
605        final_angular_velocity, final_angular_velocity_from_displacement,
606        final_angular_velocity_squared, hollow_sphere_moment_of_inertia,
607        point_mass_moment_of_inertia, radians_from_degrees, radians_from_revolutions,
608        revolutions_from_radians, rod_moment_of_inertia_about_center,
609        rod_moment_of_inertia_about_end, rotational_kinetic_energy, solid_disk_moment_of_inertia,
610        solid_sphere_moment_of_inertia, tangential_acceleration, tangential_speed,
611        thin_ring_moment_of_inertia,
612    };
613    use core::f64::consts::{PI, TAU};
614
615    const EPSILON: f64 = 1.0e-12;
616
617    fn assert_approx_eq(left: f64, right: f64) {
618        assert!(
619            (left - right).abs() <= EPSILON,
620            "left={left}, right={right}"
621        );
622    }
623
624    fn assert_some_approx_eq(value: Option<f64>, expected: f64) {
625        assert_approx_eq(value.expect("expected Some value"), expected);
626    }
627
628    #[test]
629    fn angular_conversions_cover_common_values() {
630        assert_some_approx_eq(radians_from_degrees(180.0), PI);
631        assert_some_approx_eq(degrees_from_radians(PI), 180.0);
632        assert_some_approx_eq(revolutions_from_radians(2.0 * PI), 1.0);
633        assert_some_approx_eq(radians_from_revolutions(1.0), TAU);
634    }
635
636    #[test]
637    fn angular_velocity_requires_positive_time() {
638        assert_eq!(angular_velocity(10.0, 2.0), Some(5.0));
639        assert_eq!(angular_velocity(10.0, 0.0), None);
640    }
641
642    #[test]
643    fn angular_acceleration_requires_positive_time() {
644        assert_eq!(angular_acceleration(2.0, 10.0, 4.0), Some(2.0));
645        assert_eq!(angular_acceleration(2.0, 10.0, 0.0), None);
646    }
647
648    #[test]
649    fn final_angular_velocity_requires_nonnegative_time() {
650        assert_eq!(final_angular_velocity(2.0, 3.0, 4.0), Some(14.0));
651        assert_eq!(final_angular_velocity(2.0, 3.0, -1.0), None);
652    }
653
654    #[test]
655    fn angular_displacement_requires_nonnegative_time() {
656        assert_eq!(angular_displacement(2.0, 3.0, 4.0), Some(32.0));
657        assert_eq!(angular_displacement(2.0, 3.0, -1.0), None);
658    }
659
660    #[test]
661    fn displacement_based_kinematics_cover_common_values() {
662        assert_eq!(final_angular_velocity_squared(2.0, 3.0, 4.0), Some(28.0));
663        assert_some_approx_eq(
664            final_angular_velocity_from_displacement(2.0, 3.0, 4.0),
665            28.0_f64.sqrt(),
666        );
667    }
668
669    #[test]
670    fn tangential_and_centripetal_relations_cover_common_values() {
671        assert_eq!(tangential_speed(3.0, 2.0), Some(6.0));
672        assert_eq!(tangential_speed(3.0, -2.0), None);
673        assert_eq!(angular_velocity_from_tangential_speed(6.0, 2.0), Some(3.0));
674        assert_eq!(angular_velocity_from_tangential_speed(6.0, 0.0), None);
675        assert_eq!(tangential_acceleration(3.0, 2.0), Some(6.0));
676        assert_eq!(
677            centripetal_acceleration_from_angular_velocity(3.0, 2.0),
678            Some(18.0)
679        );
680        assert_eq!(
681            centripetal_acceleration_from_tangential_speed(6.0, 2.0),
682            Some(18.0)
683        );
684    }
685
686    #[test]
687    fn moment_of_inertia_helpers_cover_common_shapes() {
688        assert_eq!(point_mass_moment_of_inertia(2.0, 3.0), Some(18.0));
689        assert_eq!(solid_disk_moment_of_inertia(2.0, 3.0), Some(9.0));
690        assert_eq!(thin_ring_moment_of_inertia(2.0, 3.0), Some(18.0));
691        assert_eq!(solid_sphere_moment_of_inertia(5.0, 2.0), Some(8.0));
692        assert_eq!(hollow_sphere_moment_of_inertia(3.0, 2.0), Some(8.0));
693        assert_eq!(rod_moment_of_inertia_about_center(12.0, 2.0), Some(4.0));
694        assert_eq!(rod_moment_of_inertia_about_end(3.0, 2.0), Some(4.0));
695    }
696
697    #[test]
698    fn angular_momentum_helpers_cover_common_values() {
699        assert_eq!(angular_momentum(4.0, 5.0), Some(20.0));
700        assert_eq!(angular_momentum(-4.0, 5.0), None);
701        assert_eq!(angular_velocity_from_angular_momentum(20.0, 4.0), Some(5.0));
702        assert_eq!(angular_velocity_from_angular_momentum(20.0, 0.0), None);
703    }
704
705    #[test]
706    fn rotational_energy_helpers_cover_common_values() {
707        assert_eq!(rotational_kinetic_energy(4.0, 5.0), Some(50.0));
708        assert_eq!(rotational_kinetic_energy(-4.0, 5.0), None);
709        assert_eq!(
710            angular_velocity_from_rotational_kinetic_energy(50.0, 4.0),
711            Some(5.0)
712        );
713        assert_eq!(
714            angular_velocity_from_rotational_kinetic_energy(-50.0, 4.0),
715            None
716        );
717    }
718
719    #[test]
720    fn angular_acceleration_from_torque_requires_positive_inertia() {
721        assert_eq!(angular_acceleration_from_torque(20.0, 4.0), Some(5.0));
722        assert_eq!(angular_acceleration_from_torque(20.0, 0.0), None);
723    }
724
725    #[test]
726    fn rotating_body_delegates_to_public_functions() {
727        let body = RotatingBody::new(4.0, 5.0).expect("expected valid rotating body");
728
729        assert_eq!(body.angular_momentum(), Some(20.0));
730        assert_eq!(body.rotational_kinetic_energy(), Some(50.0));
731        assert_eq!(RotatingBody::new(-4.0, 5.0), None);
732    }
733
734    #[test]
735    fn angular_state_advances_with_constant_acceleration() {
736        let next = AngularState::new(1.0, 2.0)
737            .expect("expected valid angular state")
738            .advanced_by_constant_acceleration(3.0, 4.0)
739            .expect("expected advanced state");
740
741        assert_eq!(
742            next,
743            AngularState {
744                angular_position: 33.0,
745                angular_velocity: 14.0,
746            }
747        );
748        assert_eq!(AngularState::new(f64::NAN, 2.0), None);
749    }
750}