deep_time/math/tan.rs
1// origin: FreeBSD /usr/src/lib/msun/src/s_tan.c
2//
3// ====================================================
4// Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
5//
6// Developed at SunPro, a Sun Microsystems, Inc. business.
7// Permission to use, copy, modify, and distribute this
8// software is freely granted, provided that this notice
9// is preserved.
10// ====================================================
11
12#![allow(clippy::indexing_slicing)]
13#![allow(clippy::excessive_precision)]
14#![allow(clippy::approx_constant)]
15#![allow(clippy::eq_op)]
16
17use super::{k_tan, rem_pio2};
18use crate::Real;
19
20/// Computes the tangent of `x` in radians (`tan(x)`).
21///
22/// Returns a value in `(-∞, +∞)`. The function has vertical asymptotes
23/// (poles) at odd multiples of π/2, where the result approaches ±∞ with
24/// the correct sign.
25///
26/// ## Special cases
27///
28/// - `tan(NaN)` returns `NaN`
29/// - `tan(±∞)` returns `NaN`
30/// - `tan(±0.0)` returns `±0.0`
31/// - `tan(k·π)` returns `0.0` (approximately, after argument reduction)
32///
33/// ## Implementation notes
34///
35/// This is a `const fn`-compatible port of the FreeBSD `libm` implementation
36/// (`s_tan.c`). It uses a fast path for `|x| ≤ π/4` (via `k_tan`), returns
37/// the input exactly for `|x| < 2⁻²⁷`, handles infinities/NaNs, and
38/// otherwise performs argument reduction with `rem_pio2` before calling
39/// `k_tan` with the appropriate parity flag.
40///
41/// ## Modifications for this crate
42///
43/// - Adapted to the generic `Real` type (which is `f64` under the hood)
44/// - Made fully `const fn` compatible
45/// - Uses the crate's own `rem_pio2` and `k_tan` implementations
46///
47/// ## Testing
48///
49/// The function is validated by a comprehensive test suite that includes:
50/// - Special values (NaN, infinities, zeros, multiples of π)
51/// - Tiny arguments (exact fast-path for `|x| < 2⁻²⁷`)
52/// - Odd symmetry (`tan(-x) == -tan(x)`)
53/// - Small arguments (fast path)
54/// - Values approaching poles (±∞ behavior)
55/// - Medium and very large arguments (argument reduction stress)
56/// - Subnormal numbers
57/// - Deterministic pseudo-random inputs
58/// - Explicit `const` evaluation checks
59///
60/// All tests pass and produce results matching `std::f64::tan` within 1 ULP.
61pub const fn tan(x: Real) -> Real {
62 let ix = (Real::to_bits(x) >> 32) as u32 & 0x7fffffff;
63
64 /* |x| ~< pi/4 */
65 if ix <= 0x3fe921fb {
66 if ix < 0x3e400000 {
67 /* |x| < 2**-27 */
68 return x;
69 }
70 return k_tan(x, Real::from_bits(0), 0); // ← must be 0 (not 1)
71 }
72
73 /* tan(Inf or NaN) is NaN */
74 if ix >= 0x7ff00000 {
75 return x - x;
76 }
77
78 /* argument reduction */
79 let (n, y0, y1) = rem_pio2(x);
80 k_tan(y0, y1, n & 1)
81}
82
83#[cfg(all(test, feature = "std"))]
84mod tan_tests {
85 use super::tan;
86 use std::f64::consts::PI;
87
88 const MAX_ULP: u64 = 1;
89
90 /// Returns the ULP (unit in the last place) difference between two `f64` values.
91 /// Correctly handles NaNs, infinities, signed zeros, and sign-bit mismatches.
92 fn ulp_diff(a: f64, b: f64) -> u64 {
93 if a.is_nan() && b.is_nan() {
94 return 0;
95 }
96 if a.is_infinite() || b.is_infinite() {
97 return if a == b { 0 } else { u64::MAX };
98 }
99
100 let a_bits = a.to_bits();
101 let b_bits = b.to_bits();
102
103 // +0.0 and -0.0 (and any zero representation) are considered identical.
104 if (a_bits | b_bits) & 0x7fff_ffff_ffff_ffff == 0 {
105 return 0;
106 }
107
108 // Non-zero values with different signs → catastrophic difference.
109 if (a_bits ^ b_bits) & 0x8000_0000_0000_0000 != 0 {
110 return u64::MAX;
111 }
112
113 a_bits.abs_diff(b_bits)
114 }
115
116 fn check(x: f64) {
117 let expected = x.tan();
118 let actual = tan(x);
119
120 if expected.is_nan() {
121 assert!(actual.is_nan(), "tan({x}) should be NaN, got {actual}");
122 return;
123 }
124
125 if expected.is_infinite() {
126 assert!(
127 actual.is_infinite(),
128 "tan({x}) should be infinite (expected {expected}, got {actual})"
129 );
130 assert_eq!(
131 actual.is_sign_positive(),
132 expected.is_sign_positive(),
133 "tan({x}) has wrong sign of infinity"
134 );
135 return;
136 }
137
138 let ulps = ulp_diff(actual, expected);
139 assert!(
140 ulps <= MAX_ULP,
141 "tan({x}) failed: expected = {expected:.17e}, got = {actual:.17e}, ULP diff = {ulps} (max allowed {MAX_ULP})"
142 );
143 }
144
145 #[test]
146 fn special_values() {
147 let cases = [
148 0.0,
149 -0.0,
150 PI / 4.0,
151 -PI / 4.0,
152 PI / 2.0,
153 -PI / 2.0,
154 PI,
155 -PI,
156 3.0 * PI / 2.0,
157 -3.0 * PI / 2.0,
158 2.0 * PI,
159 f64::INFINITY,
160 f64::NEG_INFINITY,
161 f64::NAN,
162 f64::MIN,
163 f64::MAX,
164 f64::MIN_POSITIVE,
165 ];
166 for &x in &cases {
167 check(x);
168 }
169 }
170
171 #[test]
172 fn tiny_arguments_exact() {
173 // The implementation has an explicit fast-path: |x| < 2^-27 returns x exactly.
174 // This test validates that optimization (critical for const fn and performance).
175 let threshold = f64::from_bits(0x3e400000u64 << 32); // 2^-27
176 let step = threshold * 1e-4;
177 let mut x = -threshold * 0.999;
178 while x < threshold * 0.999 {
179 assert_eq!(tan(x), x, "tiny fast-path failed at {x}");
180 x += step;
181 }
182 }
183
184 #[test]
185 fn odd_symmetry() {
186 // tan(-x) == -tan(x) must hold exactly (including for huge magnitudes)
187 for i in -500..=500 {
188 let x = (i as f64) * 0.013;
189 let tx = tan(x);
190 assert_eq!(tan(-x), -tx, "odd symmetry failed at {x}");
191
192 let x_large = x * 1e10;
193 assert_eq!(
194 tan(-x_large),
195 -tan(x_large),
196 "large odd symmetry failed at {x_large}"
197 );
198 }
199 }
200
201 #[test]
202 fn small_arguments() {
203 // |x| ≤ π/4 → uses the direct `k_tan` fast path
204 let bound = PI / 4.0;
205 for i in -12000..=12000 {
206 let x = (i as f64 / 12000.0) * bound;
207 check(x);
208 }
209 }
210
211 #[test]
212 fn approaching_poles() {
213 // The most numerically challenging region for tan: vertical asymptotes at (2k+1)π/2.
214 // We approach from both sides with progressively smaller deltas.
215 for k in -40..=40 {
216 let pole = (2 * k + 1) as f64 * PI / 2.0;
217
218 // From the left
219 for i in 1..=400 {
220 let delta = 1e-8 * (0.9999f64).powi(i);
221 check(pole - delta);
222 }
223 // From the right
224 for i in 1..=400 {
225 let delta = 1e-8 * (0.9999f64).powi(i);
226 check(pole + delta);
227 }
228 }
229 }
230
231 #[test]
232 fn medium_arguments() {
233 // Argument reduction around multiples of π
234 for k in 0..=50 {
235 let base = (k as f64) * PI;
236 for i in -250..=250 {
237 let x = base + (i as f64) * 0.0123;
238 check(x);
239 }
240 }
241 }
242
243 #[test]
244 fn large_arguments() {
245 // Heavy stress on `rem_pio2` for very large magnitudes (up to ~1e22)
246 let mut x = 1e6_f64;
247 while x < 1e22 {
248 check(x);
249 check(x + 0.23456789);
250 check(-x);
251 x *= 3.1415926535;
252 }
253 }
254
255 #[test]
256 fn subnormal_arguments() {
257 // Extremely tiny values (underflow territory)
258 let mut x = f64::MIN_POSITIVE;
259 for _ in 0..100 {
260 check(x);
261 check(-x);
262 x /= 2.0;
263 }
264 }
265
266 #[test]
267 fn randomish_tests() {
268 // Deterministic pseudo-random walk providing broad coverage
269 let mut x: f64 = 1.23456789;
270 for _ in 0..35_000 {
271 check(x);
272 x = x * 1.618033988749895 + 0.5772156649015329; // φ + Euler's γ
273 if x.abs() > 1e15 {
274 x = x.fract() * 75.0;
275 }
276 }
277 }
278
279 #[test]
280 fn const_compatibility() {
281 // Verify that the `const fn` works in a const context and produces correct values.
282 const T0: f64 = tan(0.0);
283 const T_PI4: f64 = tan(PI / 4.0);
284 const T_PI: f64 = tan(PI);
285 const T_SMALL: f64 = tan(1e-12);
286
287 assert_eq!(T0, 0.0, "tan(0.0) must be exactly 0.0");
288 assert!(
289 (T_PI4 - 1.0).abs() < 1e-14,
290 "tan(π/4) should be very close to 1.0"
291 );
292 assert!((T_PI).abs() < 1e-14, "tan(k·π) should be very close to 0.0");
293 assert!(
294 (T_SMALL - 1e-12).abs() < 1e-25,
295 "tiny values must be accurate in const context"
296 );
297 }
298}