use oxinum_core::{OxiNumError, OxiNumResult, Sign};
use super::constants::ln2;
use super::float::{BigFloat, RoundingMode};
impl BigFloat {
pub fn ln(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
assert!(prec > 0, "BigFloat precision must be > 0");
if self.is_nan() {
return Ok(BigFloat::nan(prec));
}
if self.is_infinite() {
return if self.sign == Sign::Negative {
Ok(BigFloat::nan(prec)) } else {
Ok(BigFloat::infinity(prec)) };
}
if self.is_zero() {
return Err(OxiNumError::Domain("ln of zero is undefined".into()));
}
if self.sign == Sign::Negative {
return Err(OxiNumError::Domain(
"ln of negative number is undefined for real BigFloat".into(),
));
}
let one = BigFloat::from_i64(1, prec, mode);
if self == &one {
return Ok(BigFloat::zero(prec));
}
let prec_i = prec as i64;
let big_k = self.exponent.saturating_add(prec_i);
let guard = 32u32 + prec / 16 + 8;
let work_prec = prec.saturating_add(guard);
let m = BigFloat::from_parts(
Sign::Positive,
self.mantissa.clone(),
-prec_i,
work_prec,
mode,
);
let m_f64 = m.to_f64();
let ln_m_f64 = m_f64.ln();
let ln_m = newton_ln(&m, ln_m_f64, work_prec, mode)?;
let k_float = BigFloat::from_i64(big_k, work_prec, mode);
let ln2_val = ln2(work_prec)?;
let k_times_ln2 = k_float.mul_ref_with_mode(&ln2_val, mode);
let result = ln_m.add_ref_with_mode(&k_times_ln2, mode);
Ok(result.with_precision(prec, mode))
}
}
fn newton_ln(
m: &BigFloat,
ln_m_f64: f64,
work_prec: u32,
mode: RoundingMode,
) -> OxiNumResult<BigFloat> {
let mut current_prec: u32 = 64;
let mut y = BigFloat::from_f64(ln_m_f64, current_prec)?;
let n_iters = if work_prec <= 64 {
3u32
} else {
let ratio = (work_prec as f64) / 53.0;
ratio.log2().ceil() as u32 + 3
};
let n_iters = n_iters.min(60);
for _ in 0..n_iters {
current_prec = (current_prec.saturating_mul(2)).min(work_prec);
let m_at_prec = m.clone().with_precision(current_prec, mode);
let y_at_prec = y.clone().with_precision(current_prec, mode);
let ey = y_at_prec.exp(current_prec, mode)?;
let correction = m_at_prec.div_ref_with_mode(&ey, mode)?;
let one = BigFloat::from_i64(1, current_prec, mode);
let diff = correction.sub_ref_with_mode(&one, mode);
y = y_at_prec
.add_ref_with_mode(&diff, mode)
.with_precision(current_prec, mode);
}
Ok(y.with_precision(work_prec, mode))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::native::{e_const, ln2 as ln2_const, RoundingMode};
fn mk(n: i64, prec: u32) -> BigFloat {
BigFloat::from_i64(n, prec, RoundingMode::HalfEven)
}
fn approx_eq_bits(a: &BigFloat, b: &BigFloat, tol_bits: u32) -> bool {
let diff = a.sub_ref(b).abs();
if diff.is_zero() {
return true;
}
let diff_top_exp = diff
.exponent
.saturating_add(diff.mantissa.bit_length() as i64 - 1);
diff_top_exp < -(tol_bits as i64)
}
#[test]
fn ln_one_is_zero() {
let x = mk(1, 100);
let result = x.ln(100, RoundingMode::HalfEven).expect("ln(1)");
assert!(result.is_zero(), "ln(1) should be 0, got: {result:?}");
}
#[test]
fn ln_zero_is_domain_error() {
let x = mk(0, 64);
let result = x.ln(64, RoundingMode::HalfEven);
assert!(
matches!(result, Err(OxiNumError::Domain(_))),
"Expected Domain error, got: {result:?}"
);
}
#[test]
fn ln_negative_is_domain_error() {
let x = mk(-1, 64);
let result = x.ln(64, RoundingMode::HalfEven);
assert!(
matches!(result, Err(OxiNumError::Domain(_))),
"Expected Domain error, got: {result:?}"
);
}
#[test]
fn ln_e_is_one() {
let prec = 100u32;
let e = e_const(prec).expect("e_const");
let result = e.ln(prec, RoundingMode::HalfEven).expect("ln(e)");
let one = mk(1, prec);
assert!(
approx_eq_bits(&result, &one, 85),
"ln(e) should ≈ 1, got: {} (diff from 1: {})",
result.to_f64(),
(result.to_f64() - 1.0).abs()
);
}
#[test]
fn ln2_matches_constant() {
let prec = 100u32;
let two = mk(2, prec);
let computed = two.ln(prec, RoundingMode::HalfEven).expect("ln(2)");
let expected = ln2_const(prec).expect("ln2 constant");
assert!(
approx_eq_bits(&computed, &expected, 85),
"ln(2) should match ln2 constant, diff = {}",
(computed.to_f64() - expected.to_f64()).abs()
);
}
}