myelin_geometry/
radians.rs

1use serde::{Deserialize, Serialize};
2use std::error::Error;
3use std::f64::consts::{PI, TAU};
4use std::fmt;
5
6/// A radian confined to the range of [0.0; 2π)
7#[derive(Debug, PartialEq, Copy, Clone, Default, Serialize, Deserialize)]
8pub struct Radians {
9    value: f64,
10}
11
12impl Radians {
13    /// Creates a new instance of [`Radians`].
14    ///
15    /// ### Errors
16    /// Returns a [`RadiansError`] if the given value is outside the range [0.0; 2π)
17    ///
18    /// ### Examples
19    /// ```
20    /// use myelin_geometry::Radians;
21    /// use std::f64::consts::PI;
22    ///
23    /// let rotation = Radians::try_new(PI).expect("Value was outside the range [0.0; 2π)");
24    /// ```
25    pub fn try_new(value: f64) -> Result<Self, RadiansError> {
26        const VALID_VALUES_RANGE: std::ops::Range<f64> = 0.0..TAU;
27        if VALID_VALUES_RANGE.contains(&value) {
28            Ok(Radians { value })
29        } else {
30            Err(RadiansError::OutOfRange)
31        }
32    }
33
34    /// Returns the underlying value
35    pub fn value(self) -> f64 {
36        self.value
37    }
38
39    /// Convert degrees to radians
40    ///
41    /// ### Errors
42    /// Returns a [`RadiansError`] if the given value is outside the range [0.0°; 360°)
43    ///
44    /// ### Examples
45    /// ```
46    /// use myelin_geometry::Radians;
47    /// use std::f64::consts::FRAC_PI_2;
48    /// use std::f64::consts::PI;
49    ///
50    /// use nearly_eq::assert_nearly_eq;
51    ///
52    /// assert_nearly_eq!(FRAC_PI_2, Radians::try_from_degrees(90.0).unwrap().value());
53    /// ```
54    pub fn try_from_degrees(degrees: f64) -> Result<Self, RadiansError> {
55        const MAX_DEGREES: f64 = 360.0;
56        const MAX_RADIANS: f64 = 2.0 * PI;
57
58        Radians::try_new(degrees / MAX_DEGREES * MAX_RADIANS)
59    }
60}
61
62/// The reason why a [`Radians`] instance could not be created
63#[derive(Debug)]
64pub enum RadiansError {
65    /// The given value was not in the range [0.0; 2π)
66    OutOfRange,
67}
68
69impl fmt::Display for RadiansError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "Given value is not in range [0.0; 2π)")
72    }
73}
74
75impl Error for RadiansError {}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use nearly_eq::assert_nearly_eq;
81    use std::f64::consts::PI;
82
83    #[test]
84    fn radians_new_with_negative_0_point_1_is_none() {
85        let radians = Radians::try_new(-0.1);
86        assert!(radians.is_err())
87    }
88
89    #[test]
90    fn radians_new_with_0_is_some() {
91        let radians = Radians::try_new(0.0);
92        assert!(radians.is_ok())
93    }
94
95    #[test]
96    fn radians_new_with_1_point_9_pi_is_some() {
97        let radians = Radians::try_new(1.9 * PI);
98        assert!(radians.is_ok())
99    }
100
101    #[test]
102    fn radians_new_with_2_pi_is_none() {
103        let radians = Radians::try_new(2.0 * PI);
104        assert!(radians.is_err())
105    }
106
107    #[test]
108    fn radians_value_returns_1_when_given_1() {
109        let value = 1.0;
110        let radians = Radians::try_new(value).unwrap();
111        assert_nearly_eq!(value, radians.value())
112    }
113
114    #[test]
115    fn try_from_degrees_works_with_0_as_input() {
116        let degrees = 0.0;
117        let radians = Radians::try_from_degrees(degrees).unwrap();
118        let expected = 0.0;
119        assert_nearly_eq!(expected, radians.value())
120    }
121
122    #[test]
123    fn try_from_degrees_returns_none_with_359_as_input() {
124        let degrees = 359.0;
125        let radians = Radians::try_from_degrees(degrees).unwrap();
126        let expected = (2.0 * PI) - (PI / 180.0);
127        assert_nearly_eq!(expected, radians.value())
128    }
129
130    #[test]
131    fn try_from_degrees_works_with_180_as_input() {
132        let degrees = 180.0;
133        let radians = Radians::try_from_degrees(degrees).unwrap();
134        let expected = PI;
135        assert_nearly_eq!(expected, radians.value())
136    }
137
138    #[test]
139    fn try_from_degrees_works_with_1_as_input() {
140        let degrees = 1.0;
141        let radians = Radians::try_from_degrees(degrees).unwrap();
142        let expected = PI / 180.0;
143        assert_nearly_eq!(expected, radians.value())
144    }
145
146    #[test]
147    fn try_from_degrees_works_with_360_as_input() {
148        let degrees = 360.0;
149        let radians = Radians::try_from_degrees(degrees);
150        assert!(radians.is_err());
151    }
152
153    #[test]
154    fn try_from_degrees_returns_none_with_negative_1_as_input() {
155        let degrees = -1.0;
156        let radians = Radians::try_from_degrees(degrees);
157        assert!(radians.is_err());
158    }
159
160    #[test]
161    fn try_from_degrees_returns_none_with_361_as_input() {
162        let degrees = 361.0;
163        let radians = Radians::try_from_degrees(degrees);
164        assert!(radians.is_err());
165    }
166}