Skip to main content

oxinum_float/
elementary.rs

1//! Elementary mathematical functions: exp, ln, sqrt, pow.
2//!
3//! These wrap `dashu-float`'s built-in methods with `OxiNumResult` error
4//! handling and precision-aware context management.
5
6use crate::{DBig, OxiNumError, OxiNumResult};
7use dashu_float::round::mode::HalfEven;
8use std::str::FromStr;
9
10/// Compute `e^x` with the given number of significant decimal digits.
11///
12/// # Examples
13///
14/// ```
15/// use oxinum_float::exp;
16/// use std::str::FromStr;
17/// let x = dashu_float::DBig::from_str("1.0").unwrap();
18/// let result = exp(&x, 30).unwrap();
19/// let s = result.to_string();
20/// assert!(s.starts_with("2.71828"), "exp(1) = {}", s);
21/// ```
22pub fn exp(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
23    if precision == 0 {
24        return Err(OxiNumError::Precision("precision must be > 0".into()));
25    }
26    // Special case: exp(0) = 1
27    let zero = DBig::from_str("0.0").map_err(|e| OxiNumError::Parse(format!("{e}").into()))?;
28    if *x == zero {
29        return DBig::from_str("1.0").map_err(|e| OxiNumError::Parse(format!("{e}").into()));
30    }
31    let guard_bits = precision * 4 + 20;
32    let fbig = convert_dbig_to_fbig(x, guard_bits);
33    let result = fbig.exp();
34    let dbig = fbig_to_dbig(&result, precision);
35    Ok(truncate_to_precision(dbig, precision))
36}
37
38/// Compute `ln(x)` (natural logarithm) with the given precision.
39///
40/// # Errors
41///
42/// Returns `OxiNumError::Precision` if `x <= 0`.
43///
44/// # Examples
45///
46/// ```
47/// use oxinum_float::ln;
48/// use std::str::FromStr;
49/// let x = dashu_float::DBig::from_str("2.718281828459045").unwrap();
50/// let result = ln(&x, 30).unwrap();
51/// let s = result.to_string();
52/// assert!(s.starts_with("0.99999") || s.starts_with("1.0000"), "ln(e) = {}", s);
53/// ```
54pub fn ln(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
55    if precision == 0 {
56        return Err(OxiNumError::Precision("precision must be > 0".into()));
57    }
58    // Check sign by comparing with a precision-bearing zero
59    let zero = DBig::from_str("0.0").map_err(|e| OxiNumError::Parse(format!("{e}").into()))?;
60    if *x <= zero {
61        return Err(OxiNumError::Precision("ln(x) requires x > 0".into()));
62    }
63    // Special case: ln(1) = 0
64    let one = DBig::from_str("1.0").map_err(|e| OxiNumError::Parse(format!("{e}").into()))?;
65    if *x == one {
66        return DBig::from_str("0.0").map_err(|e| OxiNumError::Parse(format!("{e}").into()));
67    }
68    let guard_bits = precision * 4 + 20;
69    let fbig = convert_dbig_to_fbig(x, guard_bits);
70    let result = fbig.ln();
71    let dbig = fbig_to_dbig(&result, precision);
72    Ok(truncate_to_precision(dbig, precision))
73}
74
75/// Compute the square root of `x` with the given precision.
76///
77/// # Errors
78///
79/// Returns `OxiNumError::Precision` if `x < 0`.
80///
81/// # Examples
82///
83/// ```
84/// use oxinum_float::sqrt;
85/// use std::str::FromStr;
86/// let x = dashu_float::DBig::from_str("2.0").unwrap();
87/// let result = sqrt(&x, 30).unwrap();
88/// let s = result.to_string();
89/// assert!(s.starts_with("1.4142135"), "sqrt(2) = {}", s);
90/// ```
91pub fn sqrt(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
92    if precision == 0 {
93        return Err(OxiNumError::Precision("precision must be > 0".into()));
94    }
95    let zero = DBig::from_str("0.0").map_err(|e| OxiNumError::Parse(format!("{e}").into()))?;
96    if *x < zero {
97        return Err(OxiNumError::Precision("sqrt(x) requires x >= 0".into()));
98    }
99    if *x == zero {
100        return Ok(zero);
101    }
102    let guard_bits = precision * 4 + 20;
103    let fbig = convert_dbig_to_fbig(x, guard_bits);
104    let result = dashu_base::SquareRoot::sqrt(&fbig);
105    let dbig = fbig_to_dbig(&result, precision);
106    Ok(truncate_to_precision(dbig, precision))
107}
108
109/// Compute `base^exp` for arbitrary float exponents.
110///
111/// Uses the identity `base^exp = e^(exp * ln(base))`.
112///
113/// # Errors
114///
115/// Returns `OxiNumError::Precision` if `base <= 0`.
116///
117/// # Examples
118///
119/// ```
120/// use oxinum_float::pow;
121/// use std::str::FromStr;
122/// let base = dashu_float::DBig::from_str("2.0").unwrap();
123/// let exp = dashu_float::DBig::from_str("10.0").unwrap();
124/// let result = pow(&base, &exp, 20).unwrap();
125/// let s = result.to_string();
126/// assert!(s.starts_with("1024"), "2^10 = {}", s);
127/// ```
128pub fn pow(base: &DBig, exponent: &DBig, precision: usize) -> OxiNumResult<DBig> {
129    if precision == 0 {
130        return Err(OxiNumError::Precision("precision must be > 0".into()));
131    }
132    let zero = DBig::from_str("0.0").map_err(|e| OxiNumError::Parse(format!("{e}").into()))?;
133    if *exponent == zero {
134        return DBig::from_str("1.0").map_err(|e| OxiNumError::Parse(format!("{e}").into()));
135    }
136    if *base <= zero {
137        return Err(OxiNumError::Precision(
138            "pow(base, exp) requires base > 0".into(),
139        ));
140    }
141    let guard_bits = precision * 4 + 20;
142    let fbig_base = convert_dbig_to_fbig(base, guard_bits);
143    let fbig_exp = convert_dbig_to_fbig(exponent, guard_bits);
144    let result = fbig_base.powf(&fbig_exp);
145    let dbig = fbig_to_dbig(&result, precision);
146    Ok(truncate_to_precision(dbig, precision))
147}
148
149// ---------------------------------------------------------------------------
150// Internal helpers
151// ---------------------------------------------------------------------------
152
153/// Convert a `DBig` (base-10) to an `FBig<HalfEven, 2>` at the given
154/// number of binary digits of precision.
155///
156/// This ensures the resulting `FBig` always has a defined precision,
157/// even for zero-valued inputs.
158pub(crate) fn convert_dbig_to_fbig(
159    value: &DBig,
160    binary_precision: usize,
161) -> dashu_float::FBig<HalfEven, 2> {
162    // Ensure we have a decimal value with at least some digits of precision
163    // by adding it to a high-precision zero. This avoids the "precision
164    // cannot be 0" panic from dashu when the input has unlimited precision.
165    let ctx = dashu_float::Context::<HalfEven>::new(binary_precision);
166    let repr = value
167        .clone()
168        .with_rounding::<HalfEven>()
169        .with_base_and_precision::<2>(binary_precision.max(10))
170        .value();
171    // Re-apply the context precision to ensure it's set
172    let result_repr = repr.repr().clone();
173    dashu_float::FBig::from_repr(result_repr, ctx)
174}
175
176/// Convert an `FBig<HalfEven, 2>` to `DBig`, handling the zero-precision edge case.
177///
178/// dashu's `to_decimal()` panics if the FBig has precision=0 (which happens
179/// for exact zero results). We detect that and return "0.0" directly.
180pub(crate) fn fbig_to_dbig(
181    fbig: &dashu_float::FBig<HalfEven, 2>,
182    decimal_precision: usize,
183) -> DBig {
184    // If digits() returns 0 (unlimited/zero), construct a precision-bearing result
185    if fbig.digits() == 0 {
186        // The value is exact zero (or similar). Return a precision-bearing zero.
187        return DBig::from_str("0.0").expect("valid literal");
188    }
189    // Use with_base_and_precision to get a decimal representation at
190    // the requested precision level.
191    let decimal_digits = decimal_precision.max(5);
192    fbig.clone()
193        .with_base_and_precision::<10>(decimal_digits)
194        .value()
195        .with_rounding::<dashu_float::round::mode::HalfAway>()
196}
197
198/// Truncate a `DBig` to `n` significant decimal digits by re-parsing.
199pub(crate) fn truncate_to_precision(value: DBig, precision: usize) -> DBig {
200    let s = value.to_string();
201    let truncated = truncate_decimal_str(&s, precision);
202    DBig::from_str(&truncated).unwrap_or(value)
203}
204
205/// Truncate a decimal string to `n` significant digits.
206///
207/// Significant digits are counted as follows:
208/// - For values >= 1 (integer part != "0"), all digits count.
209/// - For values < 1 (integer part is "0"), leading zeros after the
210///   decimal point are NOT significant (but are preserved in output).
211pub(crate) fn truncate_decimal_str(src: &str, sig_digits: usize) -> String {
212    let mut result = String::with_capacity(sig_digits + 10);
213    let mut sig_count = 0;
214
215    // Determine if the integer part is just "0" (or "-0")
216    let trimmed = src.trim_start_matches('-');
217    let integer_is_zero = trimmed.starts_with("0.") || trimmed == "0";
218
219    // Track whether we've seen any nonzero digit (for leading-zero skip)
220    let mut seen_nonzero = !integer_is_zero;
221
222    for ch in src.chars() {
223        if ch == '-' {
224            result.push(ch);
225            continue;
226        }
227        if ch == '.' {
228            result.push(ch);
229            continue;
230        }
231        // Stop at scientific notation
232        if ch == 'e' || ch == 'E' {
233            break;
234        }
235        if !ch.is_ascii_digit() {
236            continue;
237        }
238
239        if !seen_nonzero && ch == '0' {
240            // Leading zero -- not significant but kept in output
241            result.push(ch);
242            continue;
243        }
244        // From here, it's a significant digit
245        seen_nonzero = true;
246        sig_count += 1;
247        result.push(ch);
248        if sig_count >= sig_digits {
249            break;
250        }
251    }
252
253    // If we never produced any output past the sign, at least return "0"
254    let content = result.trim_start_matches('-');
255    if content.is_empty() {
256        if result.starts_with('-') {
257            return "-0".to_string();
258        }
259        return "0".to_string();
260    }
261
262    result
263}
264
265// ---------------------------------------------------------------------------
266// Tests
267// ---------------------------------------------------------------------------
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn exp_of_zero() {
275        let x = DBig::from_str("0.0").expect("ok");
276        let result = exp(&x, 30).expect("ok");
277        let s = result.to_string();
278        assert!(s.starts_with("1.0000") || s == "1", "exp(0) = {s}");
279    }
280
281    #[test]
282    fn exp_of_one() {
283        let x = DBig::from_str("1.0").expect("ok");
284        let result = exp(&x, 30).expect("ok");
285        let s = result.to_string();
286        assert!(s.starts_with("2.71828"), "exp(1) = {s}");
287    }
288
289    #[test]
290    fn ln_of_one() {
291        let x = DBig::from_str("1.0").expect("ok");
292        let result = ln(&x, 30).expect("ok");
293        let s = result.to_string();
294        // ln(1) = 0
295        let s_clean = s.trim_start_matches('-');
296        assert!(
297            s_clean.starts_with("0") && !s_clean.starts_with("0.1"),
298            "ln(1) = {s}"
299        );
300    }
301
302    #[test]
303    fn ln_negative_errors() {
304        let x = DBig::from_str("-1.0").expect("ok");
305        assert!(ln(&x, 30).is_err());
306    }
307
308    #[test]
309    fn sqrt_of_four() {
310        let x = DBig::from_str("4.0").expect("ok");
311        let result = sqrt(&x, 30).expect("ok");
312        let s = result.to_string();
313        assert!(s.starts_with("2.0000") || s == "2", "sqrt(4) = {s}");
314    }
315
316    #[test]
317    fn sqrt_of_two() {
318        let x = DBig::from_str("2.0").expect("ok");
319        let result = sqrt(&x, 30).expect("ok");
320        let s = result.to_string();
321        assert!(s.starts_with("1.4142135"), "sqrt(2) = {s}");
322    }
323
324    #[test]
325    fn sqrt_negative_errors() {
326        let x = DBig::from_str("-1.0").expect("ok");
327        assert!(sqrt(&x, 30).is_err());
328    }
329
330    #[test]
331    fn sqrt_of_zero() {
332        let x = DBig::from_str("0.0").expect("ok");
333        let result = sqrt(&x, 30).expect("ok");
334        let s = result.to_string();
335        assert!(s.starts_with("0"), "sqrt(0) = {s}");
336    }
337
338    #[test]
339    fn pow_two_to_ten() {
340        let base = DBig::from_str("2.0").expect("ok");
341        let exponent = DBig::from_str("10.0").expect("ok");
342        let result = pow(&base, &exponent, 20).expect("ok");
343        let s = result.to_string();
344        assert!(s.starts_with("1024"), "2^10 = {s}");
345    }
346
347    #[test]
348    fn pow_zero_exponent() {
349        let base = DBig::from_str("5.0").expect("ok");
350        let exponent = DBig::from_str("0.0").expect("ok");
351        let result = pow(&base, &exponent, 20).expect("ok");
352        let s = result.to_string();
353        assert!(s.starts_with("1"), "5^0 = {s}");
354    }
355
356    #[test]
357    fn precision_zero_errors() {
358        let x = DBig::from_str("1.0").expect("ok");
359        assert!(exp(&x, 0).is_err());
360        assert!(ln(&x, 0).is_err());
361        assert!(sqrt(&x, 0).is_err());
362        assert!(pow(&x, &x, 0).is_err());
363    }
364
365    #[test]
366    fn truncate_leading_zeros() {
367        let s = truncate_decimal_str("0.00123456789", 5);
368        assert_eq!(s, "0.0012345");
369    }
370
371    #[test]
372    fn truncate_integer_part() {
373        let s = truncate_decimal_str("123.456789", 6);
374        assert_eq!(s, "123.456");
375    }
376
377    #[test]
378    fn truncate_negative() {
379        let s = truncate_decimal_str("-3.14159", 4);
380        assert_eq!(s, "-3.141");
381    }
382}