Skip to main content

oxinum_float/
trig.rs

1//! Trigonometric and hyperbolic functions for arbitrary-precision floats.
2//!
3//! Implementations use Taylor series with argument reduction for
4//! trig functions, and exp-based formulas for hyperbolic functions.
5//! All functions accept `DBig` (base-10) inputs and return results
6//! at the requested precision.
7
8use crate::constants::compute_pi;
9use crate::elementary::{convert_dbig_to_fbig, exp, fbig_to_dbig, truncate_to_precision};
10use crate::{DBig, OxiNumError, OxiNumResult};
11use std::str::FromStr;
12
13// ---------------------------------------------------------------------------
14// Trigonometric functions
15// ---------------------------------------------------------------------------
16
17/// Compute `sin(x)` using Taylor series with argument reduction.
18///
19/// # Examples
20///
21/// ```
22/// use oxinum_float::sin;
23/// use std::str::FromStr;
24/// let x = dashu_float::DBig::from_str("0.0").unwrap();
25/// let result = sin(&x, 30).unwrap();
26/// let s = result.to_string();
27/// assert!(s.starts_with("0"), "sin(0) = {}", s);
28/// ```
29pub fn sin(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
30    validate_precision(precision)?;
31    let zero = make_dbig("0.0")?;
32    if *x == zero {
33        return Ok(zero);
34    }
35    // Use extra guard digits for intermediate computations
36    let guard_prec = precision + 10;
37    let reduced = reduce_argument(x, guard_prec);
38    let result = sin_taylor(&reduced, guard_prec);
39    Ok(truncate_to_precision(result, precision))
40}
41
42/// Compute `cos(x)` using Taylor series with argument reduction.
43///
44/// # Examples
45///
46/// ```
47/// use oxinum_float::cos;
48/// use std::str::FromStr;
49/// let x = dashu_float::DBig::from_str("0.0").unwrap();
50/// let result = cos(&x, 30).unwrap();
51/// let s = result.to_string();
52/// assert!(s.starts_with("1"), "cos(0) = {}", s);
53/// ```
54pub fn cos(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
55    validate_precision(precision)?;
56    let zero = make_dbig("0.0")?;
57    if *x == zero {
58        return make_dbig("1.0");
59    }
60    let guard_prec = precision + 10;
61    let reduced = reduce_argument(x, guard_prec);
62    let result = cos_taylor(&reduced, guard_prec);
63    Ok(truncate_to_precision(result, precision))
64}
65
66/// Compute `tan(x) = sin(x) / cos(x)`.
67///
68/// # Errors
69///
70/// Returns `OxiNumError::DivByZero` if `cos(x) = 0`.
71pub fn tan(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
72    validate_precision(precision)?;
73    let guard_prec = precision + 10;
74    let s = sin(x, guard_prec)?;
75    let c = cos(x, guard_prec)?;
76    let zero = make_dbig("0.0")?;
77    if c == zero {
78        return Err(OxiNumError::DivByZero);
79    }
80    let result = &s / &c;
81    Ok(truncate_to_precision(result, precision))
82}
83
84/// Compute `atan(x)` using dashu-float's binary conversion and Taylor
85/// approach internally. For |x| <= 0.5 we use the series directly;
86/// for larger |x| <= 1 we use the half-angle formula; for |x| > 1 we
87/// use the identity `atan(x) = pi/2 - atan(1/x)`.
88pub fn atan(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
89    validate_precision(precision)?;
90    let zero = make_dbig("0.0")?;
91    if *x == zero {
92        return Ok(zero);
93    }
94
95    let guard_prec = precision + 20;
96    let one = dbig_at_precision("1.0", guard_prec);
97    let neg_one = dbig_at_precision("-1.0", guard_prec);
98
99    // Carry guard digits on the argument before any division/multiplication so
100    // that low-precision inputs (e.g. a `y/x` ratio with only a few significant
101    // digits) do not collapse intermediate arithmetic to that narrow precision.
102    let x_ext = extend_precision(x, guard_prec);
103
104    let is_negative = x_ext < zero;
105    let abs_x = if is_negative {
106        &x_ext * &neg_one
107    } else {
108        x_ext.clone()
109    };
110
111    let result = if abs_x > one {
112        // atan(x) = pi/2 - atan(1/x) for x > 0
113        let pi = compute_pi(guard_prec);
114        let half_pi = &pi / &dbig_at_precision("2.0", guard_prec);
115        let recip = &one / &abs_x;
116        let atan_recip = atan_small(&recip, guard_prec);
117        &half_pi - &atan_recip
118    } else {
119        atan_small(&abs_x, guard_prec)
120    };
121
122    let final_result = if is_negative {
123        &result * &neg_one
124    } else {
125        result
126    };
127    Ok(truncate_to_precision(final_result, precision))
128}
129
130/// Compute `atan2(y, x)` with proper quadrant handling.
131///
132/// Returns the angle in radians between the positive x-axis and the
133/// point (x, y), in the range (-pi, pi].
134pub fn atan2(y: &DBig, x: &DBig, precision: usize) -> OxiNumResult<DBig> {
135    validate_precision(precision)?;
136    let zero = make_dbig("0.0")?;
137    let guard_prec = precision + 20;
138    let pi = compute_pi(guard_prec);
139
140    // Carry guard digits on both operands before forming the ratio, otherwise a
141    // division of low-precision literals (e.g. `2.0 / 3.0`) rounds to only a few
142    // significant digits and corrupts the downstream `atan` series.
143    let y_ext = extend_precision(y, guard_prec);
144    let x_ext = extend_precision(x, guard_prec);
145
146    if *x > zero {
147        let ratio = &y_ext / &x_ext;
148        atan(&ratio, precision)
149    } else if *x < zero && *y >= zero {
150        let ratio = &y_ext / &x_ext;
151        let a = atan(&ratio, guard_prec)?;
152        Ok(truncate_to_precision(&a + &pi, precision))
153    } else if *x < zero && *y < zero {
154        let ratio = &y_ext / &x_ext;
155        let a = atan(&ratio, guard_prec)?;
156        Ok(truncate_to_precision(&a - &pi, precision))
157    } else if *x == zero && *y > zero {
158        let half_pi = &pi / &make_dbig("2.0")?;
159        Ok(truncate_to_precision(half_pi, precision))
160    } else if *x == zero && *y < zero {
161        let neg_half_pi = &(&pi / &make_dbig("2.0")?) * &make_dbig("-1.0")?;
162        Ok(truncate_to_precision(neg_half_pi, precision))
163    } else {
164        // x == 0 && y == 0 -- undefined, return 0
165        Ok(zero)
166    }
167}
168
169// ---------------------------------------------------------------------------
170// Hyperbolic functions
171// ---------------------------------------------------------------------------
172
173/// Compute `sinh(x) = (e^x - e^(-x)) / 2`.
174pub fn sinh(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
175    validate_precision(precision)?;
176    let guard_prec = precision + 10;
177    let exp_x = exp(x, guard_prec)?;
178    let neg_x = x * &make_dbig("-1.0")?;
179    let exp_neg_x = exp(&neg_x, guard_prec)?;
180    let diff = &exp_x - &exp_neg_x;
181    let result = &diff / &make_dbig("2.0")?;
182    Ok(truncate_to_precision(result, precision))
183}
184
185/// Compute `cosh(x) = (e^x + e^(-x)) / 2`.
186pub fn cosh(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
187    validate_precision(precision)?;
188    let guard_prec = precision + 10;
189    let exp_x = exp(x, guard_prec)?;
190    let neg_x = x * &make_dbig("-1.0")?;
191    let exp_neg_x = exp(&neg_x, guard_prec)?;
192    let sum = &exp_x + &exp_neg_x;
193    let result = &sum / &make_dbig("2.0")?;
194    Ok(truncate_to_precision(result, precision))
195}
196
197/// Compute `tanh(x) = sinh(x) / cosh(x)`.
198pub fn tanh(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
199    validate_precision(precision)?;
200    let guard_prec = precision + 10;
201    let s = sinh(x, guard_prec)?;
202    let c = cosh(x, guard_prec)?;
203    let result = &s / &c;
204    Ok(truncate_to_precision(result, precision))
205}
206
207// ---------------------------------------------------------------------------
208// Internal: Taylor series
209// ---------------------------------------------------------------------------
210
211/// Compute sin(x) via Taylor series: sum_{k=0}^{inf} (-1)^k * x^{2k+1} / (2k+1)!
212///
213/// Assumes |x| is small (after argument reduction). All arithmetic is
214/// performed at `precision` significant digits to avoid precision loss.
215fn sin_taylor(x: &DBig, precision: usize) -> DBig {
216    let x = extend_precision(x, precision);
217    let mut term = x.clone();
218    let mut sum = x.clone();
219    let x_sq = &x * &x;
220    let neg_one = dbig_at_precision("-1.0", precision);
221
222    for k in 1..=(precision as u32 + 20) {
223        let denom_a = dbig_u32_at_precision(2 * k, precision);
224        let denom_b = dbig_u32_at_precision(2 * k + 1, precision);
225        term = &term * &x_sq;
226        term = &term / &denom_a;
227        term = &term / &denom_b;
228        term = &term * &neg_one;
229        sum = &sum + &term;
230
231        if is_negligible(&term, precision) {
232            break;
233        }
234    }
235    sum
236}
237
238/// Compute cos(x) via Taylor series: sum_{k=0}^{inf} (-1)^k * x^{2k} / (2k)!
239///
240/// Assumes |x| is small (after argument reduction). All arithmetic is
241/// performed at `precision` significant digits to avoid precision loss.
242fn cos_taylor(x: &DBig, precision: usize) -> DBig {
243    let x = extend_precision(x, precision);
244    let one = dbig_at_precision("1.0", precision);
245    let mut term = one.clone();
246    let mut sum = one;
247    let x_sq = &x * &x;
248    let neg_one = dbig_at_precision("-1.0", precision);
249
250    for k in 1..=(precision as u32 + 20) {
251        let denom_a = dbig_u32_at_precision(2 * k - 1, precision);
252        let denom_b = dbig_u32_at_precision(2 * k, precision);
253        term = &term * &x_sq;
254        term = &term / &denom_a;
255        term = &term / &denom_b;
256        term = &term * &neg_one;
257        sum = &sum + &term;
258
259        if is_negligible(&term, precision) {
260            break;
261        }
262    }
263    sum
264}
265
266/// Compute atan(x) for |x| <= 1 using argument reduction + Taylor series.
267///
268/// For faster convergence when x is close to 1, we use the identity:
269///   atan(x) = 2 * atan(x / (1 + sqrt(1 + x^2)))
270/// which reduces the argument closer to 0.
271fn atan_small(x: &DBig, precision: usize) -> DBig {
272    // Reduce argument: apply halving a few times for faster convergence.
273    // `threshold` is only used in value comparisons, so its narrow precision is
274    // harmless; `one` participates in arithmetic and must carry guard digits.
275    let threshold = make_dbig("0.5").expect("valid literal");
276    let one = dbig_at_precision("1.0", precision);
277    let two = dbig_at_precision("2.0", precision);
278
279    // Extend the input to guard precision so the halving arithmetic below
280    // (`reduced * reduced`, `1 + x^2`, `reduced / (1 + sqrt)`) is performed at
281    // full precision rather than collapsing to the input's narrow precision.
282    let mut reduced = extend_precision(x, precision);
283    let mut halvings = 0u32;
284
285    while reduced > threshold {
286        // atan(x) = 2 * atan(x / (1 + sqrt(1 + x^2)))
287        let x_sq = &reduced * &reduced;
288        let inner = &one + &x_sq;
289        // Use dashu's sqrt via binary conversion
290        let guard_bits = precision * 4 + 20;
291        let fbig = convert_dbig_to_fbig(&inner, guard_bits);
292        let sqrt_val = dashu_base::SquareRoot::sqrt(&fbig);
293        let sqrt_dbig = fbig_to_dbig(&sqrt_val, precision);
294        reduced = &reduced / &(&one + &sqrt_dbig);
295        halvings += 1;
296
297        if halvings > 50 {
298            break; // safety limit
299        }
300    }
301
302    let mut result = atan_taylor_core(&reduced, precision);
303
304    // Undo halvings: multiply by 2^halvings
305    for _ in 0..halvings {
306        result = &result * &two;
307    }
308
309    result
310}
311
312/// Raw Taylor series for atan(x), valid for |x| < 1:
313///
314/// atan(x) = x - x^3/3 + x^5/5 - x^7/7 + ...
315///
316/// All arithmetic is performed at `precision` significant digits.
317fn atan_taylor_core(x: &DBig, precision: usize) -> DBig {
318    let x = extend_precision(x, precision);
319    let mut power = x.clone();
320    let mut sum = x.clone();
321    let x_sq = &x * &x;
322    let neg_one = dbig_at_precision("-1.0", precision);
323    let mut sign = neg_one.clone();
324
325    for k in 1..=(precision as u32 * 3 + 50) {
326        power = &power * &x_sq;
327        let denom = dbig_u32_at_precision(2 * k + 1, precision);
328        let contrib = &power / &denom;
329        sum = &sum + &(&contrib * &sign);
330        sign = &sign * &neg_one;
331
332        if is_negligible(&contrib, precision) {
333            break;
334        }
335    }
336    sum
337}
338
339// ---------------------------------------------------------------------------
340// Internal: argument reduction
341// ---------------------------------------------------------------------------
342
343/// Reduce `x` modulo 2*pi so the Taylor series converges faster.
344fn reduce_argument(x: &DBig, precision: usize) -> DBig {
345    let pi = compute_pi(precision.min(195) + 5);
346    let two = dbig_at_precision("2.0", precision + 5);
347    let two_pi = (&pi * &two).with_precision(precision + 5).value();
348    let zero = dbig_at_precision("0.0", precision + 5);
349
350    if two_pi == zero {
351        return x.clone();
352    }
353
354    // Compute x mod 2*pi
355    let mut reduced = extend_precision(x, precision + 5);
356    if reduced > zero {
357        while reduced > two_pi {
358            reduced = (&reduced - &two_pi).with_precision(precision + 5).value();
359        }
360    } else {
361        while reduced < zero {
362            reduced = (&reduced + &two_pi).with_precision(precision + 5).value();
363        }
364    }
365    reduced
366}
367
368/// Check if a term is negligible for the given precision.
369fn is_negligible(term: &DBig, precision: usize) -> bool {
370    let s = term.to_string();
371    let s = s.trim_start_matches('-');
372
373    if let Some(dot_pos) = s.find('.') {
374        let integer_part = &s[..dot_pos];
375        if integer_part == "0" {
376            let frac = &s[dot_pos + 1..];
377            let leading_zeros = frac.chars().take_while(|&c| c == '0').count();
378            return leading_zeros >= precision + 3;
379        }
380    }
381    s == "0" || s == "0.0"
382}
383
384// ---------------------------------------------------------------------------
385// Internal: helpers
386// ---------------------------------------------------------------------------
387
388/// Parse a `DBig` from a string literal. Returns `OxiNumResult`.
389fn make_dbig(s: &str) -> OxiNumResult<DBig> {
390    DBig::from_str(s).map_err(|e| OxiNumError::Parse(format!("{e}").into()))
391}
392
393/// Extend a `DBig` to carry at least `precision` significant digits, so
394/// subsequent arithmetic does not round to the input's narrow precision.
395fn extend_precision(value: &DBig, precision: usize) -> DBig {
396    value.clone().with_precision(precision).value()
397}
398
399/// Parse a literal and extend it to `precision` significant digits.
400fn dbig_at_precision(s: &str, precision: usize) -> DBig {
401    let v = DBig::from_str(s).expect("valid decimal literal");
402    v.with_precision(precision).value()
403}
404
405/// Create a `DBig` from a `u32` at `precision` significant digits.
406fn dbig_u32_at_precision(n: u32, precision: usize) -> DBig {
407    let v = DBig::from_str(&format!("{n}.0")).expect("integer.0 is always a valid decimal");
408    v.with_precision(precision).value()
409}
410
411fn validate_precision(precision: usize) -> OxiNumResult<()> {
412    if precision == 0 {
413        return Err(OxiNumError::Precision("precision must be > 0".into()));
414    }
415    Ok(())
416}
417
418// ---------------------------------------------------------------------------
419// Tests
420// ---------------------------------------------------------------------------
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    #[test]
427    fn sin_of_zero() {
428        let x = make_dbig("0.0").expect("ok");
429        let result = sin(&x, 30).expect("ok");
430        assert_eq!(result, make_dbig("0.0").expect("ok"));
431    }
432
433    #[test]
434    fn cos_of_zero() {
435        let result = cos(&make_dbig("0.0").expect("ok"), 30).expect("ok");
436        let s = result.to_string();
437        assert!(s.starts_with("1"), "cos(0) = {s}");
438    }
439
440    #[test]
441    fn sin_of_pi_is_near_zero() {
442        let pi = compute_pi(40);
443        let result = sin(&pi, 20).expect("ok");
444        let s = result.to_string();
445        let s_trimmed = s.trim_start_matches('-');
446        assert!(
447            s_trimmed.starts_with("0.000") || s_trimmed == "0" || s_trimmed.starts_with("0.0"),
448            "sin(pi) = {s}"
449        );
450    }
451
452    #[test]
453    fn cos_of_pi_is_near_neg_one() {
454        let pi = compute_pi(40);
455        let result = cos(&pi, 20).expect("ok");
456        let s = result.to_string();
457        assert!(
458            s.starts_with("-1") || s.starts_with("-0.9999"),
459            "cos(pi) = {s}"
460        );
461    }
462
463    #[test]
464    fn sinh_of_zero() {
465        let x = make_dbig("0.0").expect("ok");
466        let result = sinh(&x, 30).expect("ok");
467        let s = result.to_string();
468        let s_trimmed = s.trim_start_matches('-');
469        assert!(s_trimmed.starts_with("0"), "sinh(0) = {s}");
470    }
471
472    #[test]
473    fn cosh_of_zero() {
474        let x = make_dbig("0.0").expect("ok");
475        let result = cosh(&x, 30).expect("ok");
476        let s = result.to_string();
477        assert!(
478            s.starts_with("1.000") || s.starts_with("1"),
479            "cosh(0) = {s}"
480        );
481    }
482
483    #[test]
484    fn tanh_of_zero() {
485        let x = make_dbig("0.0").expect("ok");
486        let result = tanh(&x, 30).expect("ok");
487        let s = result.to_string();
488        let s_trimmed = s.trim_start_matches('-');
489        assert!(s_trimmed.starts_with("0"), "tanh(0) = {s}");
490    }
491
492    #[test]
493    fn atan_of_zero() {
494        let x = make_dbig("0.0").expect("ok");
495        let result = atan(&x, 30).expect("ok");
496        assert_eq!(result, make_dbig("0.0").expect("ok"));
497    }
498
499    #[test]
500    fn atan_of_one_is_pi_over_4() {
501        let x = make_dbig("1.0").expect("ok");
502        let result = atan(&x, 30).expect("ok");
503        let s = result.to_string();
504        assert!(
505            s.starts_with("0.7853981"),
506            "atan(1) = {s}, expected ~0.7853981..."
507        );
508    }
509
510    #[test]
511    fn atan2_positive_x() {
512        let y = make_dbig("1.0").expect("ok");
513        let x = make_dbig("1.0").expect("ok");
514        let result = atan2(&y, &x, 20).expect("ok");
515        let s = result.to_string();
516        assert!(s.starts_with("0.785"), "atan2(1,1) = {s}");
517    }
518
519    #[test]
520    fn precision_zero_errors_trig() {
521        let x = make_dbig("1.0").expect("ok");
522        assert!(sin(&x, 0).is_err());
523        assert!(cos(&x, 0).is_err());
524        assert!(tan(&x, 0).is_err());
525        assert!(atan(&x, 0).is_err());
526        assert!(sinh(&x, 0).is_err());
527        assert!(cosh(&x, 0).is_err());
528        assert!(tanh(&x, 0).is_err());
529    }
530
531    #[test]
532    fn sin_cos_identity() {
533        // sin^2(x) + cos^2(x) = 1 to high precision
534        let x = make_dbig("0.7").expect("ok");
535        let prec = 30;
536        let s = sin(&x, prec).expect("ok");
537        let c = cos(&x, prec).expect("ok");
538        let sum = &(&s * &s) + &(&c * &c);
539        let s_str = sum.to_string();
540        // Should agree with 1.0 to at least ~25 digits
541        assert!(
542            s_str.starts_with("0.9999999999999999999999999")
543                || s_str.starts_with("1.000000000000000000000000"),
544            "sin^2(0.7) + cos^2(0.7) = {s_str}"
545        );
546    }
547
548    #[test]
549    fn sin_of_known_value() {
550        // sin(0.7) = 0.6442176872376910536726143513...
551        let x = make_dbig("0.7").expect("ok");
552        let result = sin(&x, 25).expect("ok");
553        let s = result.to_string();
554        assert!(
555            s.starts_with("0.644217687237691053672614"),
556            "sin(0.7) = {s}"
557        );
558    }
559
560    #[test]
561    fn cos_of_known_value() {
562        // cos(0.7) = 0.7648421872844884262558599...
563        let x = make_dbig("0.7").expect("ok");
564        let result = cos(&x, 25).expect("ok");
565        let s = result.to_string();
566        assert!(
567            s.starts_with("0.764842187284488426255859"),
568            "cos(0.7) = {s}"
569        );
570    }
571}