qtty_core/units/
frequency.rs

1//! Angular frequency unit aliases (`Angular / Time`).
2//!
3//! This module provides a **dimension alias** [`Frequency`].
4//!
5//! ```rust
6//! use qtty_core::angular::{Degree, Radian};
7//! use qtty_core::time::Day;
8//! use qtty_core::frequency::Frequency;
9//!
10//! let f: Frequency<Degree, Day> = Frequency::new(180.0);
11//! let f_rad: Frequency<Radian, Day> = f.to();
12//! assert!((f_rad.value() - core::f64::consts::PI).abs() < 1e-12);
13//! ```
14
15use crate::units::angular::Angular;
16use crate::units::time::Time;
17use crate::{DivDim, Per, Quantity, Unit};
18
19/// Dimension alias for angular frequency (`Angular / Time`).
20pub type FrequencyDim = DivDim<Angular, Time>;
21
22/// Marker trait for any unit with frequency dimension (`Angular / Time`).
23pub trait FrequencyUnit: Unit<Dim = FrequencyDim> {}
24impl<T: Unit<Dim = FrequencyDim>> FrequencyUnit for T {}
25
26/// A frequency quantity parameterized by angular and time units.
27///
28/// # Examples
29///
30/// ```rust
31/// use qtty_core::angular::{Degree, Radian};
32/// use qtty_core::time::{Second, Day};
33/// use qtty_core::frequency::Frequency;
34///
35/// let f1: Frequency<Degree, Second> = Frequency::new(360.0);
36/// let f2: Frequency<Radian, Day> = Frequency::new(6.28);
37/// ```
38pub type Frequency<N, D> = Quantity<Per<N, D>>;
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43    use crate::units::angular::{Degree, Degrees, MilliArcsecond, Radian};
44    use crate::units::time::{Day, Days, Year};
45    use crate::Per;
46    use approx::{assert_abs_diff_eq, assert_relative_eq};
47    use proptest::prelude::*;
48    use std::f64::consts::PI;
49
50    // ─────────────────────────────────────────────────────────────────────────────
51    // Basic frequency conversions
52    // ─────────────────────────────────────────────────────────────────────────────
53
54    #[test]
55    fn deg_per_day_to_rad_per_day() {
56        let f: Frequency<Degree, Day> = Frequency::new(180.0);
57        let f_rad: Frequency<Radian, Day> = f.to();
58        // 180 deg = π rad
59        assert_abs_diff_eq!(f_rad.value(), PI, epsilon = 1e-12);
60    }
61
62    #[test]
63    fn rad_per_day_to_deg_per_day() {
64        let f: Frequency<Radian, Day> = Frequency::new(PI);
65        let f_deg: Frequency<Degree, Day> = f.to();
66        assert_abs_diff_eq!(f_deg.value(), 180.0, epsilon = 1e-12);
67    }
68
69    #[test]
70    fn deg_per_day_to_deg_per_year() {
71        let f: Frequency<Degree, Day> = Frequency::new(1.0);
72        let f_year: Frequency<Degree, Year> = f.to();
73        // 1 deg/day = 365.2425 deg/year (tropical year)
74        assert_relative_eq!(f_year.value(), 365.2425, max_relative = 1e-6);
75    }
76
77    #[test]
78    fn deg_per_year_to_deg_per_day() {
79        let f: Frequency<Degree, Year> = Frequency::new(365.2425);
80        let f_day: Frequency<Degree, Day> = f.to();
81        assert_relative_eq!(f_day.value(), 1.0, max_relative = 1e-6);
82    }
83
84    #[test]
85    fn mas_per_day_to_deg_per_day() {
86        let f: Frequency<MilliArcsecond, Day> = Frequency::new(3_600_000.0);
87        let f_deg: Frequency<Degree, Day> = f.to();
88        // 3,600,000 mas = 1 deg
89        assert_abs_diff_eq!(f_deg.value(), 1.0, epsilon = 1e-9);
90    }
91
92    // ─────────────────────────────────────────────────────────────────────────────
93    // Per ratio behavior
94    // ─────────────────────────────────────────────────────────────────────────────
95
96    #[test]
97    fn per_ratio_deg_day() {
98        // Degree::RATIO = 1.0, Day::RATIO = 86400.0
99        // So Per<Degree, Day>::RATIO = 1.0 / 86400.0
100        let ratio = <Per<Degree, Day>>::RATIO;
101        assert_abs_diff_eq!(ratio, 1.0 / 86400.0, epsilon = 1e-12);
102    }
103
104    #[test]
105    fn per_ratio_rad_day() {
106        // Radian::RATIO = 180/π, Day::RATIO = 86400.0
107        let ratio = <Per<Radian, Day>>::RATIO;
108        assert_relative_eq!(ratio, (180.0 / PI) / 86400.0, max_relative = 1e-12);
109    }
110
111    // ─────────────────────────────────────────────────────────────────────────────
112    // Frequency * Time = Angle
113    // ─────────────────────────────────────────────────────────────────────────────
114
115    #[test]
116    fn frequency_times_time() {
117        let f: Frequency<Degree, Day> = Frequency::new(360.0);
118        let t: Days = Days::new(0.5);
119        let angle: Degrees = f * t;
120        assert_abs_diff_eq!(angle.value(), 180.0, epsilon = 1e-9);
121    }
122
123    #[test]
124    fn time_times_frequency() {
125        let f: Frequency<Degree, Day> = Frequency::new(360.0);
126        let t: Days = Days::new(0.5);
127        let angle: Degrees = t * f;
128        assert_abs_diff_eq!(angle.value(), 180.0, epsilon = 1e-9);
129    }
130
131    // ─────────────────────────────────────────────────────────────────────────────
132    // Angle / Time = Frequency
133    // ─────────────────────────────────────────────────────────────────────────────
134
135    #[test]
136    fn angle_div_time() {
137        let angle: Degrees = Degrees::new(360.0);
138        let t: Days = Days::new(1.0);
139        let f: Frequency<Degree, Day> = angle / t;
140        assert_abs_diff_eq!(f.value(), 360.0, epsilon = 1e-9);
141    }
142
143    // ─────────────────────────────────────────────────────────────────────────────
144    // Roundtrip conversions
145    // ─────────────────────────────────────────────────────────────────────────────
146
147    #[test]
148    fn roundtrip_deg_rad_per_day() {
149        let original: Frequency<Degree, Day> = Frequency::new(90.0);
150        let converted: Frequency<Radian, Day> = original.to();
151        let back: Frequency<Degree, Day> = converted.to();
152        assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-9);
153    }
154
155    // ─────────────────────────────────────────────────────────────────────────────
156    // Property-based tests
157    // ─────────────────────────────────────────────────────────────────────────────
158
159    proptest! {
160        #[test]
161        fn prop_roundtrip_deg_rad_per_day(f in 1e-6..1e6f64) {
162            let original: Frequency<Degree, Day> = Frequency::new(f);
163            let converted: Frequency<Radian, Day> = original.to();
164            let back: Frequency<Degree, Day> = converted.to();
165            prop_assert!((back.value() - original.value()).abs() < 1e-9 * f.abs().max(1.0));
166        }
167
168        #[test]
169        fn prop_frequency_time_roundtrip(
170            f_val in 1e-3..1e3f64,
171            t_val in 1e-3..1e3f64
172        ) {
173            let f: Frequency<Degree, Day> = Frequency::new(f_val);
174            let t: Days = Days::new(t_val);
175            let angle: Degrees = f * t;
176            // angle / t should give back f
177            let f_back: Frequency<Degree, Day> = angle / t;
178            prop_assert!((f_back.value() - f.value()).abs() / f.value() < 1e-12);
179        }
180    }
181}