Skip to main content

qtty_core/units/
angular_rate.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Angular rate (angular displacement per unit time) unit aliases (`Angular / Time`).
5//!
6//! This module models **angular rate** — angular displacement per unit time
7//! (e.g. radians/second, degrees/day). It does **not** model SI cycle frequency
8//! (Hz = cycles/s), which would be `T⁻¹` with dimensionless cycles.
9//!
10//! If you need Hertz-style inverse-time frequency, model it directly as
11//! `Per<Unitless, Second>` (or a named wrapper) — this module is the wrong place.
12//!
13//! ```rust
14//! use qtty_core::angular::{Degree, Radian};
15//! use qtty_core::time::Day;
16//! use qtty_core::angular_rate::AngularRate;
17//!
18//! let f: AngularRate<Degree, Day> = AngularRate::new(180.0);
19//! let f_rad: AngularRate<Radian, Day> = f.to();
20//! assert!((f_rad.value() - core::f64::consts::PI).abs() < 1e-12);
21//! ```
22
23use crate::{Per, Quantity, Unit};
24
25/// Re-export the angular rate dimension from the dimension module.
26pub use crate::dimension::AngularRate as AngularRateDimension;
27
28/// Marker trait for any unit with angular-rate dimension ([`AngularRate`](crate::AngularRate)).
29pub trait AngularRateUnit: Unit<Dim = crate::AngularRate> {}
30impl<T: Unit<Dim = crate::AngularRate>> AngularRateUnit for T {}
31
32/// An angular-rate quantity parameterized by angular and time units.
33///
34/// # Examples
35///
36/// ```rust
37/// use qtty_core::angular::{Degree, Radian};
38/// use qtty_core::time::{Second, Day};
39/// use qtty_core::angular_rate::AngularRate;
40///
41/// let f1: AngularRate<Degree, Second> = AngularRate::new(360.0);
42/// let f2: AngularRate<Radian, Day> = AngularRate::new(6.28);
43/// ```
44pub type AngularRate<N, D> = Quantity<Per<N, D>>;
45
46#[cfg(all(test, feature = "std"))]
47mod tests {
48    use super::*;
49    #[cfg(feature = "astro")]
50    use crate::units::angular::MilliArcsecond;
51    use crate::units::angular::{Degree, Degrees, Radian};
52    use crate::units::time::{Day, Days, Year};
53    use crate::Per;
54    use approx::{assert_abs_diff_eq, assert_relative_eq};
55    use proptest::prelude::*;
56    use std::f64::consts::PI;
57
58    // ─────────────────────────────────────────────────────────────────────────────
59    // Basic angular-rate conversions
60    // ─────────────────────────────────────────────────────────────────────────────
61
62    #[test]
63    fn deg_per_day_to_rad_per_day() {
64        let f: AngularRate<Degree, Day> = AngularRate::new(180.0);
65        let f_rad: AngularRate<Radian, Day> = f.to();
66        // 180 deg = π rad
67        assert_abs_diff_eq!(f_rad.value(), PI, epsilon = 1e-12);
68    }
69
70    #[test]
71    fn rad_per_day_to_deg_per_day() {
72        let f: AngularRate<Radian, Day> = AngularRate::new(PI);
73        let f_deg: AngularRate<Degree, Day> = f.to();
74        assert_abs_diff_eq!(f_deg.value(), 180.0, epsilon = 1e-12);
75    }
76
77    #[test]
78    fn deg_per_day_to_deg_per_year() {
79        let f: AngularRate<Degree, Day> = AngularRate::new(1.0);
80        let f_year: AngularRate<Degree, Year> = f.to();
81        // 1 deg/day = 365.2425 deg/year (Gregorian year)
82        assert_relative_eq!(f_year.value(), 365.2425, max_relative = 1e-6);
83    }
84
85    #[test]
86    fn deg_per_year_to_deg_per_day() {
87        let f: AngularRate<Degree, Year> = AngularRate::new(365.2425);
88        let f_day: AngularRate<Degree, Day> = f.to();
89        assert_relative_eq!(f_day.value(), 1.0, max_relative = 1e-6);
90    }
91
92    #[test]
93    #[cfg(feature = "astro")]
94    fn mas_per_day_to_deg_per_day() {
95        let f: AngularRate<MilliArcsecond, Day> = AngularRate::new(3_600_000.0);
96        let f_deg: AngularRate<Degree, Day> = f.to();
97        // 3,600,000 mas = 1 deg
98        assert_abs_diff_eq!(f_deg.value(), 1.0, epsilon = 1e-9);
99    }
100
101    // ─────────────────────────────────────────────────────────────────────────────
102    // Per ratio behavior
103    // ─────────────────────────────────────────────────────────────────────────────
104
105    #[test]
106    fn per_ratio_deg_day() {
107        // Degree::RATIO = 1.0, Day::RATIO = 86400.0
108        // So Per<Degree, Day>::RATIO = 1.0 / 86400.0
109        let ratio = <Per<Degree, Day>>::RATIO;
110        assert_abs_diff_eq!(ratio, 1.0 / 86400.0, epsilon = 1e-12);
111    }
112
113    #[test]
114    fn per_ratio_rad_day() {
115        // Radian::RATIO = 180/π, Day::RATIO = 86400.0
116        let ratio = <Per<Radian, Day>>::RATIO;
117        assert_relative_eq!(ratio, (180.0 / PI) / 86400.0, max_relative = 1e-12);
118    }
119
120    // ─────────────────────────────────────────────────────────────────────────────
121    // AngularRate * Time = Angle
122    // ─────────────────────────────────────────────────────────────────────────────
123
124    #[test]
125    fn angular_rate_times_time() {
126        let f: AngularRate<Degree, Day> = AngularRate::new(360.0);
127        let t: Days = Days::new(0.5);
128        let angle: Degrees = (f * t).to();
129        assert_abs_diff_eq!(angle.value(), 180.0, epsilon = 1e-9);
130    }
131
132    #[test]
133    fn time_times_angular_rate() {
134        let f: AngularRate<Degree, Day> = AngularRate::new(360.0);
135        let t: Days = Days::new(0.5);
136        let angle: Degrees = (t * f).to();
137        assert_abs_diff_eq!(angle.value(), 180.0, epsilon = 1e-9);
138    }
139
140    // ─────────────────────────────────────────────────────────────────────────────
141    // Angle / Time = AngularRate
142    // ─────────────────────────────────────────────────────────────────────────────
143
144    #[test]
145    fn angle_div_time() {
146        let angle: Degrees = Degrees::new(360.0);
147        let t: Days = Days::new(1.0);
148        let f: AngularRate<Degree, Day> = angle / t;
149        assert_abs_diff_eq!(f.value(), 360.0, epsilon = 1e-9);
150    }
151
152    // ─────────────────────────────────────────────────────────────────────────────
153    // Roundtrip conversions
154    // ─────────────────────────────────────────────────────────────────────────────
155
156    #[test]
157    fn roundtrip_deg_rad_per_day() {
158        let original: AngularRate<Degree, Day> = AngularRate::new(90.0);
159        let converted: AngularRate<Radian, Day> = original.to();
160        let back: AngularRate<Degree, Day> = converted.to();
161        assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-9);
162    }
163
164    // ─────────────────────────────────────────────────────────────────────────────
165    // Property-based tests
166    // ─────────────────────────────────────────────────────────────────────────────
167
168    proptest! {
169        #[test]
170        fn prop_roundtrip_deg_rad_per_day(f in 1e-6..1e6f64) {
171            let original: AngularRate<Degree, Day> = AngularRate::new(f);
172            let converted: AngularRate<Radian, Day> = original.to();
173            let back: AngularRate<Degree, Day> = converted.to();
174            prop_assert!((back.value() - original.value()).abs() < 1e-9 * f.abs().max(1.0));
175        }
176
177        #[test]
178        fn prop_angular_rate_time_roundtrip(
179            f_val in 1e-3..1e3f64,
180            t_val in 1e-3..1e3f64
181        ) {
182            let f: AngularRate<Degree, Day> = AngularRate::new(f_val);
183            let t: Days = Days::new(t_val);
184            let angle: Degrees = (f * t).to();
185            // angle / t should give back f
186            let f_back: AngularRate<Degree, Day> = angle / t;
187            prop_assert!((f_back.value() - f.value()).abs() / f.value() < 1e-12);
188        }
189    }
190}