use crate::error::Result;
use crate::eval::{eval, Context};
use crate::expr::Expr::{self, *};
use crate::simplify::simplify;
use crate::symbolic::differentiate;
pub const MAX_LOPITAL: usize = 6;
#[derive(Debug, Clone, PartialEq)]
pub enum LimitValue {
Finite(f64),
PosInfinity,
NegInfinity,
DoesNotExist,
}
impl std::fmt::Display for LimitValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LimitValue::Finite(v) => write!(f, "{}", fmt_value(*v)),
LimitValue::PosInfinity => write!(f, "+∞"),
LimitValue::NegInfinity => write!(f, "-∞"),
LimitValue::DoesNotExist => write!(f, "does not exist"),
}
}
}
fn fmt_value(v: f64) -> String {
if v == v.trunc() && v.abs() < 1e15 {
format!("{}", v as i64)
} else {
format!("{:.10}", v)
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
}
}
pub fn limit(expr: &Expr, var: &str, point: f64) -> Result<LimitValue> {
let mut steps = Vec::new();
limit_rec(expr, var, point, 0, &mut steps)
}
pub fn limit_steps(expr: &Expr, var: &str, point: f64) -> Result<Vec<String>> {
let mut steps = vec![format!(
"limit of {} as {} → {}",
simplify(expr),
var,
fmt_point(point)
)];
let v = limit_rec(expr, var, point, 0, &mut steps)?;
steps.push(format!("limit = {}", v));
Ok(steps)
}
fn limit_rec(
expr: &Expr,
var: &str,
point: f64,
depth: usize,
steps: &mut Vec<String>,
) -> Result<LimitValue> {
let s = simplify(expr);
if let Some(v) = try_substitute(&s, var, point) {
if v.is_finite() {
let snapped = snap_num(v);
steps.push(format!(
"substitute {} = {} → {}",
var,
fmt_point(point),
LimitValue::Finite(snapped)
));
return Ok(LimitValue::Finite(snapped));
}
if v.is_infinite() && point.is_infinite() {
let lv = if v > 0.0 {
LimitValue::PosInfinity
} else {
LimitValue::NegInfinity
};
steps.push(format!(
"substitute {} = {} → {}",
var,
fmt_point(point),
lv
));
return Ok(lv);
}
if depth == 0 {
steps.push(format!(
"substitution at {} = {} is indeterminate",
var,
fmt_point(point)
));
}
} else if depth == 0 {
steps.push(format!(
"substitution at {} = {} is undefined — analysing the form",
var,
fmt_point(point)
));
}
if let Div(num, den) = &s {
if depth < MAX_LOPITAL {
let nv = try_substitute(num, var, point);
let dv = try_substitute(den, var, point);
let indeterminate = match (nv, dv) {
(Some(n), Some(d)) => {
(is_zero_num(n) && is_zero_num(d))
|| (n.is_infinite() && d.is_infinite())
}
_ => false,
};
if indeterminate {
let dnum = simplify(&differentiate(num, var)?);
let dden = simplify(&differentiate(den, var)?);
let form = match (nv, dv) {
(Some(n), Some(_)) if is_zero_num(n) => "0/0".to_string(),
_ => "∞/∞".to_string(),
};
steps.push(format!(
"{} form — apply L'Hôpital's rule (differentiate top and bottom):",
form
));
let quotient = Expr::div(dnum, dden);
steps.push(format!("= limit of {}", quotient));
return limit_rec("ient, var, point, depth + 1, steps);
}
}
}
let v = probe_limit(&s, var, point)?;
steps.push(format!("numeric probe near {} = {} → {}", var, fmt_point(point), v));
Ok(v)
}
fn try_substitute(expr: &Expr, var: &str, val: f64) -> Option<f64> {
let mut ctx = Context::standard();
ctx.set(var, val);
eval(expr, &ctx).ok()
}
fn is_zero_num(v: f64) -> bool {
v.abs() < 1e-12
}
fn probe_limit(expr: &Expr, var: &str, point: f64) -> Result<LimitValue> {
let at_infinity = point.is_infinite();
let points: Vec<f64> = if at_infinity {
let sign = if point == f64::INFINITY { 1.0 } else { -1.0 };
(1..=15).map(|k| sign * 10f64.powi(k)).collect()
} else {
let scale = point.abs().max(1.0);
let mut pts = Vec::new();
for k in 2..=10 {
let h = 10f64.powi(-k) * scale;
pts.push(point - h);
pts.push(point + h);
}
pts
};
let values: Vec<Option<f64>> = points
.iter()
.map(|&x| try_substitute(expr, var, x).filter(|v| v.is_finite()))
.collect();
let (left, right): (Vec<Option<f64>>, Vec<Option<f64>>) = if at_infinity {
(values.clone(), values.clone())
} else {
let n = values.len();
(values[..n / 2].to_vec(), values[n / 2..].to_vec())
};
let l_est = side_estimate(&left);
let r_est = side_estimate(&right);
if let (Some(l), Some(r)) = (l_est, r_est) {
let tol = 1e-3 * l.abs().max(r.abs()).max(1.0);
if (l - r).abs() <= tol {
return Ok(LimitValue::Finite(snap_num(r)));
}
return Ok(LimitValue::DoesNotExist);
}
let l_pole = pole_sign(&left);
let r_pole = pole_sign(&right);
if at_infinity {
if let Some(sign) = l_pole.or(r_pole) {
return Ok(if sign > 0.0 {
LimitValue::PosInfinity
} else {
LimitValue::NegInfinity
});
}
} else if let (Some(ls), Some(rs)) = (l_pole, r_pole) {
return Ok(if ls == rs {
if ls > 0.0 {
LimitValue::PosInfinity
} else {
LimitValue::NegInfinity
}
} else {
LimitValue::DoesNotExist
});
} else if l_pole.is_some() || r_pole.is_some() {
return Ok(LimitValue::DoesNotExist);
}
Ok(LimitValue::DoesNotExist)
}
fn pole_sign(values: &[Option<f64>]) -> Option<f64> {
let finite: Vec<f64> = values.iter().filter_map(|v| *v).collect();
let last = *finite.last()?;
if last.abs() <= 1e9 {
return None;
}
Some(if last > 0.0 { 1.0 } else { -1.0 })
}
fn side_estimate(values: &[Option<f64>]) -> Option<f64> {
let finite: Vec<f64> = values.iter().filter_map(|v| *v).collect();
if finite.len() < 2 {
return None;
}
let last = finite[finite.len() - 1];
let prev = finite[finite.len() - 2];
let tol = 1e-3 * last.abs().max(1.0);
if (last - prev).abs() <= tol {
Some(last)
} else {
None
}
}
fn snap_num(v: f64) -> f64 {
if !v.is_finite() {
return v;
}
if (v - v.round()).abs() < 1e-9 * v.abs().max(1.0) {
return v.round();
}
let mag = v.abs();
let digits = 11 - mag.log10().floor() as i32;
if !(-20..=20).contains(&digits) {
return v;
}
let factor = 10f64.powi(digits);
(v * factor).round() / factor
}
pub fn fmt_point(point: f64) -> String {
if point == f64::INFINITY {
"+∞".to_string()
} else if point == f64::NEG_INFINITY {
"-∞".to_string()
} else {
format!("{}", point)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::Parser;
fn lim(src: &str, point: f64) -> LimitValue {
let e = Parser::parse(src).unwrap();
limit(&e, "x", point).unwrap()
}
#[test]
fn removable_singularity_lhopital() {
assert_eq!(lim("(x^2 - 1)/(x - 1)", 1.0), LimitValue::Finite(2.0));
}
#[test]
fn sin_x_over_x() {
assert_eq!(lim("sin(x)/x", 0.0), LimitValue::Finite(1.0));
}
#[test]
fn one_minus_cos_over_x_squared() {
assert_eq!(lim("(1 - cos(x))/x^2", 0.0), LimitValue::Finite(0.5));
}
#[test]
fn continuous_direct_substitution() {
assert_eq!(lim("x^2 + 1", 3.0), LimitValue::Finite(10.0));
assert_eq!(lim("sin(x)", 0.0), LimitValue::Finite(0.0));
}
#[test]
fn poles() {
assert_eq!(lim("1/x^2", 0.0), LimitValue::PosInfinity);
assert_eq!(lim("-1/x^2", 0.0), LimitValue::NegInfinity);
assert_eq!(lim("1/(x-2)^2", 2.0), LimitValue::PosInfinity);
}
#[test]
fn opposite_sides_diverge() {
assert_eq!(lim("1/x", 0.0), LimitValue::DoesNotExist);
}
#[test]
fn oscillation_does_not_exist() {
assert_eq!(lim("cos(1/x)", 0.0), LimitValue::DoesNotExist);
}
#[test]
fn limits_at_infinity() {
assert_eq!(lim("1/x", f64::INFINITY), LimitValue::Finite(0.0));
assert_eq!(lim("(2*x + 1)/(x + 5)", f64::INFINITY), LimitValue::Finite(2.0));
assert_eq!(lim("x^2", f64::INFINITY), LimitValue::PosInfinity);
assert_eq!(lim("exp(-x)", f64::INFINITY), LimitValue::Finite(0.0));
assert_eq!(lim("exp(x)", f64::NEG_INFINITY), LimitValue::Finite(0.0));
assert_eq!(lim("-x", f64::INFINITY), LimitValue::NegInfinity);
}
#[test]
fn infinity_over_infinity_lhopital() {
assert_eq!(lim("x/exp(x)", f64::INFINITY), LimitValue::Finite(0.0));
}
#[test]
fn sin_over_x_at_infinity() {
assert_eq!(lim("sin(x)/x", f64::INFINITY), LimitValue::Finite(0.0));
}
#[test]
fn sqrt_form_at_infinity() {
assert_eq!(lim("sqrt(x^2 + x) - x", f64::INFINITY), LimitValue::Finite(0.5));
}
#[test]
fn value_snapping() {
assert_eq!(snap_num(0.9999999999999983), 1.0);
assert_eq!(snap_num(2.0000000000000004), 2.0);
assert_eq!(snap_num(0.49999999999999994), 0.5);
}
#[test]
fn limit_steps_output() {
let e = Parser::parse("sin(x)/x").unwrap();
let steps = limit_steps(&e, "x", 0.0).unwrap();
assert!(steps[0].contains("sin(x)/x"));
assert!(steps.iter().any(|s| s.contains("L'Hôpital")));
assert!(steps.last().unwrap().contains("1"));
}
#[test]
fn fmt_point_display() {
assert_eq!(fmt_point(0.0), "0");
assert_eq!(fmt_point(2.5), "2.5");
assert_eq!(fmt_point(f64::INFINITY), "+∞");
assert_eq!(fmt_point(f64::NEG_INFINITY), "-∞");
}
#[test]
fn exp_over_x_forms() {
assert_eq!(lim("exp(x)/x^2", f64::INFINITY), LimitValue::PosInfinity);
}
#[test]
fn nested_lhopital_depth() {
let v = lim("(x - sin(x))/x^3", 0.0);
assert_eq!(v, LimitValue::Finite(snap_num(1.0 / 6.0)));
}
#[test]
fn negative_finite_limit() {
let v = lim("-(x^2 - 4)/(x - 2)", 2.0);
assert_eq!(v, LimitValue::Finite(-4.0));
}
#[test]
fn limit_at_negative_infinity() {
let v = lim("1/x", f64::NEG_INFINITY);
assert_eq!(v, LimitValue::Finite(0.0));
}
#[test]
fn exp_grows_at_infinity() {
let v = lim("exp(x)", f64::INFINITY);
assert_eq!(v, LimitValue::PosInfinity);
}
#[test]
fn exp_shrinks_at_neg_infinity() {
let v = lim("exp(x)", f64::NEG_INFINITY);
assert_eq!(v, LimitValue::Finite(0.0));
}
#[test]
fn log_diverges_at_infinity() {
let v = lim("ln(x)", f64::INFINITY);
assert_eq!(v, LimitValue::PosInfinity);
}
#[test]
fn one_sided_disagreement_exp() {
let v = lim("exp(1/x)", 0.0);
assert_ne!(v, LimitValue::PosInfinity, "should not be +∞ for 2-sided limit");
}
#[test]
fn discontinuous_floor_at_integer() {
let v = lim("floor(x)", 1.0);
assert_eq!(v, LimitValue::Finite(1.0));
}
#[test]
fn sign_at_zero() {
let v = lim("sign(x)", 0.0);
assert_eq!(v, LimitValue::Finite(1.0));
}
#[test]
fn constant_limit() {
let v = lim("42", 5.0);
assert_eq!(v, LimitValue::Finite(42.0));
}
#[test]
fn polynomial_limit_by_substitution() {
let v = lim("x^3 - 2*x + 1", 3.0);
assert_eq!(v, LimitValue::Finite(22.0));
}
#[test]
fn rational_function_continuous_point() {
let v = lim("(x^2 + 1)/(x + 1)", 2.0);
assert_eq!(v, LimitValue::Finite(snap_num(5.0 / 3.0)));
}
#[test]
fn lhopital_exponential_form() {
let v = lim("(exp(x) - 1)/x", 0.0);
assert_eq!(v, LimitValue::Finite(1.0));
}
#[test]
fn lhopital_logarithmic_form() {
let v = lim("ln(x + 1)/x", 0.0);
assert_eq!(v, LimitValue::Finite(1.0));
}
#[test]
fn snap_num_negative_value() {
assert_eq!(snap_num(-3.0), -3.0);
assert_eq!(snap_num(-3.9999999999), -4.0);
}
#[test]
fn fmt_point_negative_infinity() {
assert_eq!(fmt_point(f64::NEG_INFINITY), "-∞");
assert_eq!(fmt_point(0.0), "0");
}
#[test]
fn limit_steps_nonempty() {
let e = Parser::parse("(x^2 - 1)/(x - 1)").unwrap();
let steps = limit_steps(&e, "x", 1.0).unwrap();
assert!(!steps.is_empty());
let combined = steps.join(" ");
assert!(combined.contains("L'Hôpital") || combined.contains("substitution") || combined.contains("0/0"),
"steps should mention technique: {}", combined);
}
#[test]
fn limit_does_not_exist_oscillation() {
let v = lim("sin(1/x)", 0.0);
assert_eq!(v, LimitValue::DoesNotExist);
}
}