1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::f64::consts::{PI, TAU};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Angle {
11 radians: f64,
12}
13
14impl Angle {
15 #[must_use]
17 pub const fn from_radians(radians: f64) -> Self {
18 Self { radians }
19 }
20
21 #[must_use]
23 pub const fn from_degrees(degrees: f64) -> Self {
24 Self::from_radians(degrees.to_radians())
25 }
26
27 #[must_use]
29 pub const fn radians(self) -> f64 {
30 self.radians
31 }
32
33 #[must_use]
35 pub const fn degrees(self) -> f64 {
36 self.radians.to_degrees()
37 }
38
39 #[must_use]
41 pub fn normalized(self) -> Self {
42 Self::from_radians(self.radians.rem_euclid(TAU))
43 }
44
45 #[must_use]
47 pub fn normalized_signed(self) -> Self {
48 let radians = self.normalized().radians();
49
50 if radians > PI {
51 Self::from_radians(radians - TAU)
52 } else {
53 Self::from_radians(radians)
54 }
55 }
56
57 #[must_use]
59 pub fn sin(self) -> f64 {
60 self.radians.sin()
61 }
62
63 #[must_use]
65 pub fn cos(self) -> f64 {
66 self.radians.cos()
67 }
68
69 #[must_use]
71 pub fn tan(self) -> f64 {
72 self.radians.tan()
73 }
74
75 #[must_use]
77 pub fn sin_cos(self) -> (f64, f64) {
78 self.radians.sin_cos()
79 }
80}
81
82#[must_use]
84pub const fn degrees_to_radians(degrees: f64) -> f64 {
85 degrees.to_radians()
86}
87
88#[must_use]
90pub const fn radians_to_degrees(radians: f64) -> f64 {
91 radians.to_degrees()
92}
93
94#[must_use]
96pub fn normalize_degrees(degrees: f64) -> f64 {
97 degrees.rem_euclid(360.0)
98}
99
100#[must_use]
102pub fn normalize_radians(radians: f64) -> f64 {
103 radians.rem_euclid(TAU)
104}
105
106#[must_use]
108pub fn sin_deg(degrees: f64) -> f64 {
109 degrees_to_radians(degrees).sin()
110}
111
112#[must_use]
114pub fn cos_deg(degrees: f64) -> f64 {
115 degrees_to_radians(degrees).cos()
116}
117
118#[must_use]
120pub fn tan_deg(degrees: f64) -> f64 {
121 degrees_to_radians(degrees).tan()
122}
123
124pub mod prelude;
125
126#[cfg(test)]
127mod tests {
128 use super::{
129 Angle, cos_deg, degrees_to_radians, normalize_degrees, normalize_radians,
130 radians_to_degrees, sin_deg, tan_deg,
131 };
132 use core::f64::consts::{PI, TAU};
133
134 fn assert_close(left: f64, right: f64) {
135 assert!((left - right).abs() < 1.0e-12, "left={left}, right={right}");
136 }
137
138 #[test]
139 fn converts_between_degrees_and_radians() {
140 let straight = Angle::from_degrees(180.0);
141 let right = Angle::from_radians(PI / 2.0);
142
143 assert_close(straight.radians(), PI);
144 assert_close(right.degrees(), 90.0);
145 assert_close(degrees_to_radians(60.0), PI / 3.0);
146 assert_close(radians_to_degrees(PI / 6.0), 30.0);
147 }
148
149 #[test]
150 fn normalizes_angles_and_evaluates_trig_helpers() {
151 let wrapped = Angle::from_degrees(-30.0).normalized();
152 let signed = Angle::from_degrees(450.0).normalized_signed();
153 let (sine, cosine) = Angle::from_degrees(30.0).sin_cos();
154
155 assert_close(wrapped.degrees(), 330.0);
156 assert_close(signed.degrees(), 90.0);
157 assert_close(sine, 0.5);
158 assert_close(cosine, (3.0_f64).sqrt() / 2.0);
159 assert_close(sin_deg(30.0), 0.5);
160 assert_close(cos_deg(60.0), 0.5);
161 assert_close(tan_deg(45.0), 1.0);
162 assert_close(normalize_degrees(-90.0), 270.0);
163 assert_close(normalize_radians(-PI / 2.0), TAU - (PI / 2.0));
164 }
165}