use crate::error::{MathError, Result};
use crate::expr::Expr;
use crate::poly::{to_poly, Poly, Term};
fn snap(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
}
fn num(v: f64) -> Expr {
Expr::num(snap(v))
}
fn coeffs(poly: &Poly, var: &str) -> Option<[f64; 3]> {
let mut out = [0.0_f64; 3];
for Term { coeff, monomial } in poly {
match monomial.get(var).copied().unwrap_or(0) {
0 if monomial.is_empty() => out[0] += coeff,
1 if monomial.len() == 1 => out[1] += coeff,
2 if monomial.len() == 1 => out[2] += coeff,
_ => return None,
}
}
Some(out)
}
fn complex_root_expr(re: f64, im: f64) -> Expr {
let re = snap(re);
let im = snap(im);
let imag = |mag: f64| -> Expr {
if mag == 1.0 {
Expr::var("i")
} else {
Expr::mul(Expr::num(mag), Expr::var("i"))
}
};
if im == 0.0 {
return Expr::num(re);
}
if re == 0.0 {
return if im < 0.0 { Expr::neg(imag(-im)) } else { imag(im) };
}
if im < 0.0 {
Expr::sub(Expr::num(re), imag(-im))
} else {
Expr::add(Expr::num(re), imag(im))
}
}
pub fn solve_symbolic(expr: &Expr, var: &str) -> Result<Vec<Expr>> {
let poly = to_poly(&crate::simplify::simplify(expr)).ok_or_else(|| {
MathError::InvalidArgument(format!(
"'{expr}' is not a polynomial in {var}; use the numeric solve instead"
))
})?;
let [c, b, a] = coeffs(&poly, var).ok_or_else(|| {
MathError::InvalidArgument(format!(
"degree > 2 in {var} is not supported symbolically; use poly-roots for numeric roots"
))
})?;
if a != 0.0 {
let d = b * b - 4.0 * a * c;
let d_scale = (b * b).abs().max((4.0 * a * c).abs()).max(1.0);
let (r1, r2) = if d.abs() < 1e-12 * d_scale {
let r = -b / (2.0 * a);
(r, r)
} else if d > 0.0 {
let s = d.sqrt();
((-b + s) / (2.0 * a), (-b - s) / (2.0 * a))
} else {
let im = (-d).sqrt() / (2.0 * a);
let re = -b / (2.0 * a);
return Ok(vec![complex_root_expr(re, im), complex_root_expr(re, -im)]);
};
Ok(vec![Expr::num(snap(r1)), Expr::num(snap(r2))])
} else if b != 0.0 {
Ok(vec![Expr::num(snap(-c / b))])
} else {
Err(MathError::InvalidArgument(
"equation has no variable dependence (constant = 0)".into(),
))
}
}
pub fn qsolve_steps_str(input: &str) -> Result<Vec<String>> {
let (lhs, rhs) = match input.split_once('=') {
Some((l, r)) => (l.trim(), Some(r.trim())),
None => (input.trim(), None),
};
let e = crate::parser::Parser::parse(lhs)?;
let e = match rhs {
Some(r) => {
let r = crate::parser::Parser::parse(r)?;
Expr::sub(e, r)
}
None => e,
};
let mut vars = e.variables();
vars.sort();
vars.dedup();
if vars.len() != 1 {
return Err(MathError::InvalidArgument(format!(
"qsolve needs exactly one variable (found: {}); numeric solve handles systems",
if vars.is_empty() { "none".into() } else { vars.join(", ") }
)));
}
let var = vars.remove(0);
let poly = to_poly(&crate::simplify::simplify(&e)).ok_or_else(|| {
MathError::InvalidArgument(format!("'{lhs}' is not a polynomial in {var}"))
})?;
let [c, b, a] = coeffs(&poly, &var).ok_or_else(|| {
MathError::InvalidArgument(format!(
"degree > 2 in {var} is not supported symbolically; use poly-roots for numeric roots"
))
})?;
let mut steps = Vec::new();
let eq_display = match rhs {
Some(r) => format!("{lhs} = {r}"),
None => format!("{lhs} = 0"),
};
steps.push(format!("qsolve: {eq_display} (in {var})"));
let deg = if a != 0.0 {
2
} else if b != 0.0 {
1
} else {
return Err(MathError::InvalidArgument(
"equation has no variable dependence (constant = 0)".into(),
));
};
let fmt_coeff = |v: f64| -> String { format!("{}", num(v)) };
if deg == 1 {
steps.push(format!(
"linear: {}*{} + {} = 0",
fmt_coeff(b),
var,
fmt_coeff(c)
));
let roots = solve_symbolic(&e, &var)?;
steps.push(format!("{var} = {}", roots[0]));
return Ok(steps);
}
let d = b * b - 4.0 * a * c;
let d_scale = (b * b).abs().max((4.0 * a * c).abs()).max(1.0);
steps.push(format!(
"coefficients: a = {}, b = {}, c = {}",
fmt_coeff(a),
fmt_coeff(b),
fmt_coeff(c)
));
steps.push(format!(
"discriminant: d = b^2 - 4ac = {} ({})",
fmt_coeff(d),
if d.abs() < 1e-12 * d_scale {
"= 0 (double root)".to_string()
} else if d > 0.0 {
"> 0 (two real roots)".to_string()
} else {
"< 0 (complex-conjugate roots)".to_string()
}
));
let roots = solve_symbolic(&e, &var)?;
let joined: Vec<String> = roots.iter().map(|r| format!("{var} = {r}")).collect();
steps.push(joined.join(", "));
Ok(steps)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::Parser;
fn roots(input: &str) -> Vec<String> {
qsolve_steps_str(input)
.unwrap()
.pop()
.unwrap()
.split(", ")
.map(|s| s.to_string())
.collect()
}
#[test]
fn two_integer_roots() {
assert_eq!(roots("x^2 - 4"), vec!["x = 2", "x = -2"]);
assert_eq!(roots("x^2 = 2x + 3"), vec!["x = 3", "x = -1"]);
assert_eq!(roots("x^2 - x - 6"), vec!["x = 3", "x = -2"]);
}
#[test]
fn double_root() {
assert_eq!(roots("(x - 3)^2"), vec!["x = 3", "x = 3"]);
assert_eq!(roots("x^2"), vec!["x = 0", "x = 0"]);
}
#[test]
fn complex_conjugate_roots() {
assert_eq!(roots("x^2 + 1"), vec!["x = i", "x = -i"]);
assert_eq!(roots("x^2 - 2x + 5"), vec!["x = 1 + 2*i", "x = 1 - 2*i"]);
assert_eq!(roots("x^2 + 4x + 5"), vec!["x = -2 + i", "x = -2 - i"]);
}
#[test]
fn linear() {
assert_eq!(roots("2x - 6"), vec!["x = 3"]);
assert_eq!(roots("5 - x"), vec!["x = 5"]);
}
#[test]
fn irrational_discriminant_gives_decimals() {
let r = roots("x^2 - 2");
let x1: f64 = r[0].strip_prefix("x = ").unwrap().parse().unwrap();
assert!((x1 - std::f64::consts::SQRT_2).abs() < 1e-10, "got {r:?}");
}
#[test]
fn complex_roots_satisfy_equation_via_ceval() {
let z = crate::ceval::eval_complex_str("(1 + 2*i)^2 - 2*(1 + 2*i) + 5").unwrap();
assert!(z.re.abs() < 1e-9 && z.im.abs() < 1e-9, "got {z}");
}
#[test]
fn rejects_non_polynomial_and_high_degree() {
assert!(qsolve_steps_str("sin(x) = 0").is_err());
assert!(qsolve_steps_str("x^3 - 1").is_err());
assert!(qsolve_steps_str("x*y = 1").is_err()); assert!(qsolve_steps_str("1/x = 2").is_err()); }
#[test]
fn api_level_solve_symbolic() {
let e = Parser::parse("x^2 - 2x + 5").unwrap();
let rs = solve_symbolic(&e, "x").unwrap();
assert_eq!(rs.len(), 2);
assert_eq!(rs[0].to_string(), "1 + 2*i");
}
}