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