Skip to main content

dashu_float/math/
trig.rs

1//! Trigonometric functions, built on top of the cached constants π/2 and the real
2//! [`exp`](crate::FBig::exp)/[`ln`](crate::FBig::ln) primitives:
3//!
4//! - Circular: `sin`, `cos`, `tan`, `sin_cos`, and their inverses `asin`, `acos`, `atan`.
5//!
6//! Argument reduction to the first quadrant reuses the cached π so that repeated
7//! calls at increasing precision extend the shared constant state.
8
9use crate::{
10    error::{assert_limited_precision, FpError},
11    fbig::FBig,
12    math::{
13        cache::{reborrow_cache, ConstCache},
14        FpResult,
15    },
16    repr::{Context, Repr, Word},
17    round::{Round, Rounded},
18};
19use core::cmp::Ordering;
20use core::convert::TryFrom;
21use dashu_base::{AbsOrd, Approximation::Exact, RemEuclid, Sign};
22use dashu_int::IBig;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25enum Quadrant {
26    First,
27    Second,
28    Third,
29    Fourth,
30}
31
32/// Build a `Normal` result equal to `±0`, preserving the sign of `x` (used by `sin`/`tan`/`sin_cos`
33/// at zero input, where `sin(-0) = -0` and `tan(-0) = -0`).
34fn signed_zero_normal<R: Round, const B: Word>(
35    ctx: &Context<R>,
36    x: &Repr<B>,
37) -> FpResult<FBig<R, B>> {
38    let zero = if x.is_neg_zero() {
39        Repr::neg_zero()
40    } else {
41        Repr::zero()
42    };
43    Ok(Exact(FBig::<R, B>::new(zero, *ctx)))
44}
45
46impl<R: Round> Context<R> {
47    /// Calculate the internal work context for trigonometric functions based on input magnitude.
48    ///
49    /// This ensures we have enough guard digits to prevent catastrophic cancellation
50    /// during range reduction for large inputs.
51    fn compute_work_context_trig<const B: Word>(self, x: &Repr<B>) -> Self {
52        // x_mag estimates m = floor(log_BASE(|x|))
53        let x_mag = (x.exponent.saturating_add(x.digits_ub() as isize)).max(0) as usize;
54
55        // We need precision + log10(x) digits to maintain 'precision' digits after reduction.
56        // We add a base of 50 guard digits, plus 10% of x_mag for very large arguments
57        // to account for cumulative errors in division and multiplication during reduction.
58        let extra_guards = 50 + x_mag / 10;
59        let work_precision = self
60            .precision
61            .saturating_add(x_mag)
62            .saturating_add(extra_guards);
63        Self::new(work_precision)
64    }
65
66    /// Reduces the argument to the first quadrant for trigonometric evaluation.
67    /// Returns the internal work context, the reduced argument `r`, and the quadrant `k % 4`.
68    fn reduce_to_quadrant<const B: Word>(
69        self,
70        x: &Repr<B>,
71        mut cache: Option<&mut ConstCache>,
72    ) -> (Self, FBig<R, B>, Quadrant) {
73        let work_context = self.compute_work_context_trig(x);
74        let x_f = FBig::<R, B>::new(work_context.repr_round(x.clone()).value(), work_context);
75
76        let pi = work_context.pi::<B>(reborrow_cache(&mut cache)).value();
77        let half_pi = &pi / 2;
78        let x_scaled: FBig<R, B> = &x_f / &half_pi;
79        let k_f = x_scaled.round();
80        let r = x_f - &k_f * half_pi;
81        // `k_f` is the integer nearest `x_scaled`, so it's exact (or a signed zero
82        // for a tiny argument in (-1, 0), which `IBig::try_from` treats as plain 0).
83        let k = IBig::try_from(k_f).expect("k_f is an exact integer or signed zero");
84
85        let k_mod_4_big = k.rem_euclid(IBig::from(4));
86        let Ok(k_mod_4_int) = i8::try_from(k_mod_4_big) else {
87            unreachable!("k % 4 is always in [0, 3]");
88        };
89        let quadrant = match k_mod_4_int {
90            0 => Quadrant::First,
91            1 => Quadrant::Second,
92            2 => Quadrant::Third,
93            3 => Quadrant::Fourth,
94            _ => unreachable!(),
95        };
96
97        (work_context, r, quadrant)
98    }
99
100    /// Calculate the sine of the floating point representation.
101    pub fn sin<const B: Word>(
102        &self,
103        x: &Repr<B>,
104        mut cache: Option<&mut ConstCache>,
105    ) -> FpResult<FBig<R, B>> {
106        if x.is_infinite() {
107            return Err(FpError::InfiniteInput);
108        }
109        assert_limited_precision(self.precision);
110
111        if x.significand.is_zero() {
112            // sin(±0) = ±0
113            return signed_zero_normal(self, x);
114        }
115
116        let (work_context, r, quadrant) = self.reduce_to_quadrant(x, reborrow_cache(&mut cache));
117
118        // 3. Evaluate the reduced series based on the quadrant
119        let res = match quadrant {
120            Quadrant::First => work_context.sin_internal(&r),
121            Quadrant::Second => work_context.cos_internal(&r),
122            Quadrant::Third => -work_context.sin_internal(&r),
123            Quadrant::Fourth => -work_context.cos_internal(&r),
124        };
125        Ok(res.with_precision(self.precision))
126    }
127
128    /// Internal Taylor series for sine: S(x) = x - x^3/3! + x^5/5! - ...
129    fn sin_internal<const B: Word>(self, x: &FBig<R, B>) -> FBig<R, B> {
130        if x.repr.significand.is_zero() {
131            return FBig::ZERO;
132        }
133        let x2 = x.sqr();
134        let mut sum = x.clone();
135        let mut term = x.clone();
136        let mut k = 1usize;
137        let threshold = sum.sub_ulp();
138        loop {
139            term *= &x2;
140            term /= (2 * k) * (2 * k + 1);
141            if term.abs_cmp(&threshold).is_le() {
142                break;
143            }
144            if k % 2 == 1 {
145                sum -= &term;
146            } else {
147                sum += &term;
148            }
149            k += 1;
150        }
151        sum
152    }
153
154    /// Calculate the cosine of the floating point representation.
155    pub fn cos<const B: Word>(
156        &self,
157        x: &Repr<B>,
158        mut cache: Option<&mut ConstCache>,
159    ) -> FpResult<FBig<R, B>> {
160        if x.is_infinite() {
161            return Err(FpError::InfiniteInput);
162        }
163        assert_limited_precision(self.precision);
164
165        if x.significand.is_zero() {
166            // cos(±0) = 1
167            return Ok(FBig::<R, B>::ONE.with_precision(self.precision));
168        }
169
170        let (work_context, r, quadrant) = self.reduce_to_quadrant(x, reborrow_cache(&mut cache));
171
172        // 3. Evaluate the reduced series based on the quadrant
173        let res = match quadrant {
174            Quadrant::First => work_context.cos_internal(&r),
175            Quadrant::Second => -work_context.sin_internal(&r),
176            Quadrant::Third => -work_context.cos_internal(&r),
177            Quadrant::Fourth => work_context.sin_internal(&r),
178        };
179        Ok(res.with_precision(self.precision))
180    }
181
182    /// Internal Taylor series for cosine: C(x) = 1 - x^2/2! + x^4/4! - ...
183    fn cos_internal<const B: Word>(self, x: &FBig<R, B>) -> FBig<R, B> {
184        if x.repr.significand.is_zero() {
185            return FBig::ONE.with_precision(self.precision).value();
186        }
187        let x2 = x.sqr();
188        let mut sum = FBig::<R, B>::ONE.with_precision(self.precision).value();
189        let mut term = sum.clone();
190        let mut k = 1usize;
191        let threshold = sum.sub_ulp();
192        loop {
193            term *= &x2;
194            term /= (2 * k) * (2 * k - 1);
195            if term.abs_cmp(&threshold).is_le() {
196                break;
197            }
198            if k % 2 == 1 {
199                sum -= &term;
200            } else {
201                sum += &term;
202            }
203            k += 1;
204        }
205        sum
206    }
207
208    /// Calculate both the sine and cosine of the floating point representation.
209    ///
210    /// This is more efficient than calling `sin` and `cos` separately.
211    pub fn sin_cos<const B: Word>(
212        &self,
213        x: &Repr<B>,
214        mut cache: Option<&mut ConstCache>,
215    ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
216        if x.is_infinite() {
217            return (Err(FpError::InfiniteInput), Err(FpError::InfiniteInput));
218        }
219        assert_limited_precision(self.precision);
220
221        if x.significand.is_zero() {
222            // sin(±0) = ±0, cos(±0) = 1
223            let s = signed_zero_normal(self, x);
224            let c = Ok(FBig::<R, B>::ONE.with_precision(self.precision));
225            return (s, c);
226        }
227
228        let (work_context, r, quadrant) = self.reduce_to_quadrant(x, reborrow_cache(&mut cache));
229
230        let (sin_r, cos_r) = work_context.sin_cos_internal(&r);
231
232        let (s, c) = match quadrant {
233            Quadrant::First => (sin_r, cos_r),
234            Quadrant::Second => (cos_r, -sin_r),
235            Quadrant::Third => (-sin_r, -cos_r),
236            Quadrant::Fourth => (-cos_r, sin_r),
237        };
238
239        (Ok(s.with_precision(self.precision)), Ok(c.with_precision(self.precision)))
240    }
241
242    /// Simultaneously evaluate Taylor series for sine and cosine.
243    pub(crate) fn sin_cos_internal<const B: Word>(
244        self,
245        x: &FBig<R, B>,
246    ) -> (FBig<R, B>, FBig<R, B>) {
247        if x.repr.significand.is_zero() {
248            return (FBig::ZERO, FBig::ONE.with_precision(self.precision).value());
249        }
250        let x2 = x.sqr();
251        let mut sin_sum = x.clone();
252        let mut cos_sum = FBig::<R, B>::ONE.with_precision(self.precision).value();
253        let mut sin_term = x.clone();
254        let mut cos_term = cos_sum.clone();
255        let mut k = 1usize;
256        let sin_threshold = sin_sum.sub_ulp();
257        let cos_threshold = cos_sum.sub_ulp();
258        loop {
259            cos_term *= &x2;
260            cos_term /= (2 * k) * (2 * k - 1);
261            sin_term *= &x2;
262            sin_term /= (2 * k) * (2 * k + 1);
263
264            if sin_term.abs_cmp(&sin_threshold).is_le() && cos_term.abs_cmp(&cos_threshold).is_le()
265            {
266                break;
267            }
268
269            if k % 2 == 1 {
270                cos_sum -= &cos_term;
271                sin_sum -= &sin_term;
272            } else {
273                cos_sum += &cos_term;
274                sin_sum += &sin_term;
275            }
276            k += 1;
277        }
278        (sin_sum, cos_sum)
279    }
280
281    /// Calculate the tangent of the floating point representation.
282    ///
283    /// # Note
284    /// Near odd multiples of π/2, the result is an infinity (returned as a value, not an error).
285    pub fn tan<const B: Word>(
286        &self,
287        x: &Repr<B>,
288        mut cache: Option<&mut ConstCache>,
289    ) -> FpResult<FBig<R, B>> {
290        if x.is_infinite() {
291            return Err(FpError::InfiniteInput);
292        }
293        assert_limited_precision(self.precision);
294
295        if x.significand.is_zero() {
296            // tan(±0) = ±0
297            return signed_zero_normal(self, x);
298        }
299
300        let (work_context, r, quadrant) = self.reduce_to_quadrant(x, reborrow_cache(&mut cache));
301        let (sin_r, cos_r) = work_context.sin_cos_internal(&r);
302
303        let (s_f, c_f) = match quadrant {
304            Quadrant::First => (sin_r, cos_r),
305            Quadrant::Second => (cos_r, -sin_r),
306            Quadrant::Third => (-sin_r, -cos_r),
307            Quadrant::Fourth => (-cos_r, sin_r),
308        };
309
310        if c_f.repr.is_pos_zero() {
311            // tan hits a pole: the result is an infinity with the sign of the numerator.
312            let inf = if s_f.sign() == Sign::Negative {
313                Repr::neg_infinity()
314            } else {
315                Repr::infinity()
316            };
317            return Ok(Rounded::Exact(FBig::new(inf, *self)));
318        }
319        self.div(&s_f.repr, &c_f.repr)
320            .map(|r| r.and_then(|f| f.with_precision(self.precision)))
321    }
322
323    /// Calculate the arcsine of the floating point representation.
324    ///
325    /// # Methodology
326    /// Uses the identity: `asin(x) = atan(x / sqrt(1 - x^2))`
327    /// Returns `Err(OutOfDomain)` if `|x| > 1`.
328    pub fn asin<const B: Word>(
329        &self,
330        x: &Repr<B>,
331        mut cache: Option<&mut ConstCache>,
332    ) -> FpResult<FBig<R, B>> {
333        if x.is_infinite() {
334            return Err(FpError::InfiniteInput);
335        }
336        assert_limited_precision(self.precision);
337
338        let x_orig = FBig::<R, B>::new(x.clone(), *self);
339        // Domain check: |x| must be <= 1
340        if x_orig.abs_cmp(&FBig::ONE).is_gt() {
341            return Err(FpError::OutOfDomain);
342        }
343
344        let guard_digits = 50;
345        let work_precision = self.precision + guard_digits;
346        let work_context = Self::new(work_precision);
347
348        let x_f = FBig::<R, B>::new(work_context.repr_round(x.clone()).value(), work_context);
349
350        let res = work_context.asin_internal(&x_f, reborrow_cache(&mut cache));
351        Ok(res.with_precision(self.precision))
352    }
353
354    fn asin_internal<const B: Word>(
355        self,
356        x_f: &FBig<R, B>,
357        mut cache: Option<&mut ConstCache>,
358    ) -> FBig<R, B> {
359        let one = FBig::<R, B>::ONE.with_precision(self.precision).value();
360        let x2 = x_f.sqr();
361        let d = self.unwrap_fp(self.sqrt(&(one - x2).repr));
362
363        if d.repr.is_pos_zero() || d.repr.is_neg_zero() {
364            // |x| = 1 exactly (d = sqrt(1 - x²) = ±0); asin(±1) = ±π/2 regardless of rounding
365            // mode. Catch `-0` too: under roundTowardNegative `1 - 1` cancels to `-0`, sqrt(-0) =
366            // `-0`, and the general path would divide by `-0` → `-∞` and panic. (For |x| < 1,
367            // `d` is strictly positive, so only the exact-boundary x reaches this branch.)
368            let pi = self.pi::<B>(reborrow_cache(&mut cache)).value();
369            let half_pi: FBig<R, B> = pi / 2;
370            if x_f.sign() == Sign::Positive {
371                return half_pi;
372            }
373            return -half_pi;
374        }
375
376        self.atan_with_reduction(&(x_f / d), reborrow_cache(&mut cache))
377    }
378
379    /// Calculate the arccosine of the floating point representation.
380    ///
381    /// # Methodology
382    /// Uses the identity: `acos(x) = pi/2 - asin(x)`.
383    /// Higher precision is used internally to avoid catastrophic cancellation near x ≈ 1.
384    pub fn acos<const B: Word>(
385        &self,
386        x: &Repr<B>,
387        mut cache: Option<&mut ConstCache>,
388    ) -> FpResult<FBig<R, B>> {
389        if x.is_infinite() {
390            return Err(FpError::InfiniteInput);
391        }
392        assert_limited_precision(self.precision);
393
394        let x_orig = FBig::<R, B>::new(x.clone(), *self);
395        // Domain check: |x| must be <= 1
396        if x_orig.abs_cmp(&FBig::ONE).is_gt() {
397            return Err(FpError::OutOfDomain);
398        }
399
400        let guard_digits = 50;
401        let work_precision = self.precision + guard_digits;
402        let work_context = Self::new(work_precision);
403
404        let x_f = FBig::<R, B>::new(work_context.repr_round(x.clone()).value(), work_context);
405
406        let asin_x = work_context.asin_internal(&x_f, reborrow_cache(&mut cache));
407        let pi = work_context.pi::<B>(reborrow_cache(&mut cache)).value();
408        let half_pi: FBig<R, B> = pi / 2;
409        let res: FBig<R, B> = half_pi - asin_x;
410        Ok(res.with_precision(self.precision))
411    }
412
413    /// Calculate the arctangent of the floating point representation.
414    pub fn atan<const B: Word>(
415        &self,
416        x: &Repr<B>,
417        mut cache: Option<&mut ConstCache>,
418    ) -> FpResult<FBig<R, B>> {
419        if x.is_infinite() {
420            // atan(±inf) = ±π/2 — preserved (a well-defined finite result for an infinite input)
421            let pi = self.pi::<B>(reborrow_cache(&mut cache)).value();
422            let half_pi: FBig<R, B> = pi / 2;
423            let res: FBig<R, B> = if x.sign() == Sign::Positive {
424                half_pi
425            } else {
426                -half_pi
427            };
428            return Ok(res.with_precision(self.precision));
429        }
430
431        assert_limited_precision(self.precision);
432
433        if x.significand.is_zero() {
434            // atan(±0) = ±0
435            return signed_zero_normal(self, x);
436        }
437
438        let guard_digits = 50;
439        let work_precision = self.precision + guard_digits;
440        let work_context = Self::new(work_precision);
441
442        let x_f = FBig::<R, B>::new(work_context.repr_round(x.clone()).value(), work_context);
443        let res = work_context.atan_with_reduction(&x_f, reborrow_cache(&mut cache));
444        Ok(res.with_precision(self.precision))
445    }
446
447    /// Internal arctangent that includes range reduction but no guard digit allocation.
448    fn atan_with_reduction<const B: Word>(
449        self,
450        x_f: &FBig<R, B>,
451        mut cache: Option<&mut ConstCache>,
452    ) -> FBig<R, B> {
453        let sign = x_f.sign();
454        let mut x_abs = x_f.clone();
455        if sign == Sign::Negative {
456            x_abs = -x_abs;
457        }
458        let mut res = if x_abs >= FBig::<R, B>::ONE.with_precision(self.precision).value() {
459            let pi = self.pi::<B>(reborrow_cache(&mut cache)).value();
460            let inv_x = FBig::<R, B>::ONE.with_precision(self.precision).value() / x_abs;
461            (pi / 2) - self.atan_internal(&inv_x)
462        } else {
463            self.atan_internal(&x_abs)
464        };
465        if sign == Sign::Negative {
466            res = -res;
467        }
468        res
469    }
470
471    /// Internal series for arctangent.
472    /// Evaluates the Euler series for arctangent.
473    fn atan_internal<const B: Word>(self, x: &FBig<R, B>) -> FBig<R, B> {
474        // Euler's series for atan(x)
475        let x2 = x.sqr();
476        let one_plus_x2 = FBig::ONE + &x2;
477        let mut term = x / &one_plus_x2;
478        let mut sum = term.clone();
479        let factor = (2 * &x2) / one_plus_x2;
480        let mut n = 1usize;
481        let threshold = sum.sub_ulp();
482        loop {
483            term *= &factor;
484            term *= n;
485            term /= 2 * n + 1;
486            if term.abs_cmp(&threshold).is_le() {
487                break;
488            }
489            sum += &term;
490            n += 1;
491        }
492        sum
493    }
494
495    /// Calculate the arctangent of y / x.
496    ///
497    /// Handles signed infinities according to IEEE 754 standards.
498    /// Returns `Err(OutOfDomain)` if both arguments are zero.
499    pub fn atan2<const B: Word>(
500        &self,
501        y: &Repr<B>,
502        x: &Repr<B>,
503        mut cache: Option<&mut ConstCache>,
504    ) -> FpResult<FBig<R, B>> {
505        if y.is_finite() && x.is_finite() && y.significand.is_zero() && x.significand.is_zero() {
506            return Err(FpError::OutOfDomain);
507        }
508
509        assert_limited_precision(self.precision);
510
511        let guard_digits = 50;
512        let work_precision = self.precision + guard_digits;
513        let work_context = Self::new(work_precision);
514
515        // Handle Infinities according to IEEE 754
516        if y.is_infinite() || x.is_infinite() {
517            let (sy, sx) = (y.sign() == Sign::Positive, x.sign() == Sign::Positive);
518            let res: FBig<R, B> = match (y.is_infinite(), x.is_infinite(), sy, sx) {
519                (true, true, true, true) => {
520                    work_context.pi::<B>(reborrow_cache(&mut cache)).value() / 4
521                }
522                (true, true, true, false) => {
523                    work_context.pi::<B>(reborrow_cache(&mut cache)).value() * 3 / 4
524                }
525                (true, true, false, true) => {
526                    let pi4: FBig<R, B> =
527                        work_context.pi::<B>(reborrow_cache(&mut cache)).value() / 4;
528                    -pi4
529                }
530                (true, true, false, false) => {
531                    let pi34: FBig<R, B> =
532                        work_context.pi::<B>(reborrow_cache(&mut cache)).value() * 3 / 4;
533                    -pi34
534                }
535                (true, false, true, _) => {
536                    work_context.pi::<B>(reborrow_cache(&mut cache)).value() / 2
537                }
538                (true, false, false, _) => {
539                    let half_pi: FBig<R, B> =
540                        work_context.pi::<B>(reborrow_cache(&mut cache)).value() / 2;
541                    -half_pi
542                }
543                (false, true, _, true) => {
544                    // atan2(±finite, +inf) = ±0 (signed zero of y)
545                    if sy {
546                        FBig::<R, B>::ZERO.with_precision(work_precision).value()
547                    } else {
548                        FBig::<R, B>::new(Repr::neg_zero(), work_context)
549                            .with_precision(work_precision)
550                            .value()
551                    }
552                }
553                (false, true, true, false) => {
554                    work_context.pi::<B>(reborrow_cache(&mut cache)).value()
555                }
556                (false, true, false, false) => {
557                    -work_context.pi::<B>(reborrow_cache(&mut cache)).value()
558                }
559                _ => unreachable!(),
560            };
561            return Ok(res.with_precision(self.precision));
562        }
563
564        let y_f = FBig::<R, B>::new(work_context.repr_round(y.clone()).value(), work_context);
565        let x_f = FBig::<R, B>::new(work_context.repr_round(x.clone()).value(), work_context);
566
567        match x_f.cmp(&FBig::<R, B>::ZERO) {
568            Ordering::Greater => {
569                let res =
570                    work_context.atan_with_reduction(&(y_f / x_f), reborrow_cache(&mut cache));
571                Ok(res.with_precision(self.precision))
572            }
573            Ordering::Less => {
574                let pi = work_context.pi::<B>(reborrow_cache(&mut cache)).value();
575                let y_sign = y_f.sign();
576                let atan_yx =
577                    work_context.atan_with_reduction(&(y_f / x_f), reborrow_cache(&mut cache));
578                let res = if y_sign == Sign::Positive {
579                    atan_yx + pi
580                } else {
581                    atan_yx - pi
582                };
583                Ok(res.with_precision(self.precision))
584            }
585            Ordering::Equal => {
586                // x == 0 case
587                let pi = work_context.pi::<B>(reborrow_cache(&mut cache)).value();
588                let half_pi: FBig<R, B> = pi / 2;
589                if y_f > FBig::<R, B>::ZERO {
590                    Ok(half_pi.with_precision(self.precision))
591                } else {
592                    let res = -half_pi;
593                    Ok(res.with_precision(self.precision))
594                }
595            }
596        }
597    }
598}
599
600impl<R: Round, const B: Word> FBig<R, B> {
601    /// Calculate the sine of the floating point number.
602    ///
603    /// # Panics
604    /// Panics if the input is infinite.
605    #[inline]
606    pub fn sin(&self) -> Self {
607        self.context.unwrap_fp(self.context.sin(&self.repr, None))
608    }
609
610    /// Calculate the cosine of the floating point number.
611    ///
612    /// # Panics
613    /// Panics if the input is infinite.
614    #[inline]
615    pub fn cos(&self) -> Self {
616        self.context.unwrap_fp(self.context.cos(&self.repr, None))
617    }
618
619    /// Calculate both the sine and cosine of the floating point number.
620    ///
621    /// This is more efficient than calling `sin` and `cos` separately.
622    ///
623    /// # Panics
624    /// Panics if the input is infinite.
625    #[inline]
626    pub fn sin_cos(&self) -> (Self, Self) {
627        let (s, c) = self.context.sin_cos(&self.repr, None);
628        (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
629    }
630
631    /// Calculate the tangent of the floating point number.
632    ///
633    /// At odd multiples of π/2 the result is an infinity (returned as a value).
634    ///
635    /// # Panics
636    /// Panics if the input is infinite.
637    #[inline]
638    pub fn tan(&self) -> Self {
639        self.context.unwrap_fp(self.context.tan(&self.repr, None))
640    }
641
642    /// Calculate the arcsine of the floating point number.
643    ///
644    /// # Panics
645    /// Panics if the input is infinite or `|self| > 1` (out of domain).
646    #[inline]
647    pub fn asin(&self) -> Self {
648        self.context.unwrap_fp(self.context.asin(&self.repr, None))
649    }
650
651    /// Calculate the arccosine of the floating point number.
652    ///
653    /// # Panics
654    /// Panics if the input is infinite or `|self| > 1` (out of domain).
655    #[inline]
656    pub fn acos(&self) -> Self {
657        self.context.unwrap_fp(self.context.acos(&self.repr, None))
658    }
659
660    /// Calculate the arctangent of the floating point number. `atan(±inf) = ±π/2`.
661    #[inline]
662    pub fn atan(&self) -> Self {
663        self.context.unwrap_fp(self.context.atan(&self.repr, None))
664    }
665
666    /// Calculate the arctangent of `self / x`.
667    ///
668    /// # Panics
669    /// Panics if both arguments are zero.
670    #[inline]
671    pub fn atan2(&self, x: &Self) -> Self {
672        self.context
673            .unwrap_fp(self.context.atan2(&self.repr, &x.repr, None))
674    }
675}
676
677impl<R: Round> Context<R> {
678    /// Calculate π using the Chudnovsky algorithm with binary splitting.
679    ///
680    /// The Chudnovsky algorithm is one of the most efficient methods for
681    /// high-precision π calculation, providing ~14.18 decimal digits per term.
682    ///
683    /// # Methodology
684    /// We use Binary Splitting to evaluate the series. This technique transforms
685    /// the linear-time summation into a recursive tree evaluation. By combining
686    /// terms into large products, it allows the library to leverage fast
687    /// multiplication algorithms (like Toom-3 or FFT) as the numbers grow,
688    /// leading to significant performance gains over simple iterative summation.
689    #[must_use]
690    pub fn pi<const B: Word>(&self, cache: Option<&mut ConstCache>) -> Rounded<FBig<R, B>> {
691        if let Some(c) = cache {
692            return c.pi::<B, R>(self.precision);
693        }
694
695        // No shared cache: compute via a one-shot ConstCache so the Chudnovsky series
696        // and the 426880·√10005·Q/T finalization live in exactly one place (see
697        // ConstCache::pi), instead of being duplicated here.
698        let mut fresh = ConstCache::new();
699        fresh.pi::<B, R>(self.precision)
700    }
701}
702
703impl<R: Round, const B: Word> FBig<R, B> {
704    /// Calculate π with the given precision and the default rounding mode.
705    #[inline]
706    #[must_use]
707    pub fn pi(precision: usize) -> Self {
708        Context::<R>::new(precision).pi(None).value()
709    }
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715    use crate::round::mode;
716    use crate::DBig;
717    use core::str::FromStr;
718
719    #[test]
720    fn test_atan_infinity_is_preserved() {
721        let ctx = Context::<mode::HalfEven>::new(53);
722        // atan(±inf) = ±π/2 — a finite result, preserved (not an error)
723        let r = ctx.atan::<2>(&Repr::<2>::infinity(), None).unwrap().value();
724        assert!(r.repr().sign() == Sign::Positive);
725        // it should be approximately π/2
726        assert!(r > FBig::<mode::HalfEven>::ONE);
727    }
728
729    /// Regression: a tiny *negative* argument used to panic in `reduce_to_quadrant`.
730    /// `round()` of a value in (-1, 0) yields signed zero (exponent sentinel -1),
731    /// which `IBig::try_from` now accepts as plain 0.
732    #[test]
733    fn test_trig_tiny_negative_no_panic() {
734        let ctx = Context::<mode::HalfAway>::new(30);
735        for &e in &[-1isize, -2, -10, -30] {
736            // x = -1 * BASE^e, a tiny negative value
737            let x = Repr::<10>::new(IBig::from(-1), e);
738            let s = ctx.sin::<10>(&x, None).unwrap().value();
739            let c = ctx.cos::<10>(&x, None).unwrap().value();
740            let (ss, cc) = ctx.sin_cos::<10>(&x, None);
741            let ss = ss.unwrap().value();
742            let cc = cc.unwrap().value();
743            // sin is odd, cos is even: sin(x) ≈ x (negative), cos(x) ≈ 1
744            assert_eq!(s.sign(), Sign::Negative);
745            assert_eq!(c.sign(), Sign::Positive);
746            assert_eq!(ss.sign(), Sign::Negative);
747            assert_eq!(cc.sign(), Sign::Positive);
748        }
749    }
750
751    /// Regression: a 49-digit significand at precision 100 used to assertion-fail in `Context::sin`'s
752    /// rounding logic (found during fuzzing). Promoted here from the excluded `fuzz/` crate so it runs
753    /// in CI; rewritten to the current `Context::sin` API.
754    #[test]
755    fn test_sin_many_digit_rounding_no_panic() {
756        let x = DBig::from_str("-5.525474318981006776603409487767135633516667011547942409467e-3")
757            .unwrap();
758        let ctx = Context::<mode::HalfEven>::new(100);
759        let s = ctx.sin::<10>(x.repr(), None).unwrap().value();
760        // sin(x) ≈ x for a small negative x — completing without panicking is the regression guard.
761        assert_eq!(s.sign(), Sign::Negative);
762    }
763}