use symplex::prelude::*;
fn verify_roots_are_zeros(poly: &Ex, var: &Ex, roots: &[Ex], label: &str) {
for (i, root) in roots.iter().enumerate() {
let val = poly.subs(var, root).eval().simplify();
if val.is_zero_structural() {
continue;
}
let val_expanded = val.expand().eval().simplify();
if val_expanded.is_zero_structural() {
continue;
}
match val.eval_complex64() {
Ok((re, im)) => {
let mag = (re * re + im * im).sqrt();
assert!(
mag < 1e-6,
"{label}: root[{i}] = {root} does not satisfy equation \
(residual = {re} + {im}i, |r| = {mag})"
);
}
Err(_) => {
match val.eval_f64() {
Ok(v) => {
assert!(
v.abs() < 1e-6,
"{label}: root[{i}] = {root} does not satisfy equation \
(residual = {v})"
);
}
Err(_) => {
panic!(
"{label}: root[{i}] = {root} could not be verified \
(substitution gave {val}, could not evaluate numerically)"
);
}
}
}
}
}
}
fn eval_at(expr: &Ex, var: &Ex, pt: i64) -> f64 {
expr.subs_i64(var, pt)
.eval()
.eval_f64()
.unwrap_or_else(|e| panic!("eval_at({expr}, {var}={pt}) failed: {e}"))
}
fn approx(a: f64, b: f64, tol: f64) -> bool {
if a.is_nan() && b.is_nan() {
return true;
}
if a.is_infinite() && b.is_infinite() {
return a.signum() == b.signum();
}
let scale = a.abs().max(b.abs()).max(1.0);
(a - b).abs() < tol * scale
}
fn assert_equal_at_points(a: &Ex, b: &Ex, var: &Ex, label: &str) {
for &pt in &[-5i64, -3, -2, -1, 0, 1, 2, 3, 5, 7, 10] {
let va = eval_at(a, var, pt);
let vb = eval_at(b, var, pt);
assert!(
approx(va, vb, 1e-9),
"{label}: mismatch at {var}={pt}: {va} vs {vb}"
);
}
}
#[test]
fn solve_quadratic_basic_verify_by_substitution() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 - 5 * x + 6);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 2, "expected 2 roots");
verify_roots_are_zeros(&poly, &x, &roots, "x²-5x+6");
let strs: Vec<String> = roots.iter().map(|r| format!("{r}")).collect();
assert!(strs.contains(&"2".to_string()), "missing root 2: {strs:?}");
assert!(strs.contains(&"3".to_string()), "missing root 3: {strs:?}");
}
#[test]
fn solve_quadratic_irrational_roots_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 - 2);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 2, "expected 2 roots for x²-2");
verify_roots_are_zeros(&poly, &x, &roots, "x²-2");
for r in &roots {
let v = r.eval_f64().expect("root should eval to f64");
assert!(
approx(v.abs(), std::f64::consts::SQRT_2, 1e-10),
"root {r} ≈ {v} should be near ±√2"
);
}
}
#[test]
fn solve_quadratic_repeated_root_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 - 6 * x + 9);
let roots = poly.solve_or_empty(&x);
assert!(!roots.is_empty(), "expected at least 1 root for (x-3)²");
verify_roots_are_zeros(&poly, &x, &roots, "(x-3)²");
for r in &roots {
assert_eq!(format!("{r}"), "3", "all roots should be 3, got {r}");
}
}
#[test]
fn solve_quadratic_complex_roots_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 + 4);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 2, "expected 2 complex roots for x²+4");
verify_roots_are_zeros(&poly, &x, &roots, "x²+4");
}
#[test]
fn solve_quadratic_discriminant_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 + 2 * x + 1);
let roots = poly.solve_or_empty(&x);
assert!(!roots.is_empty(), "expected root(s) for (x+1)²");
verify_roots_are_zeros(&poly, &x, &roots, "(x+1)²");
for r in &roots {
assert_eq!(format!("{r}"), "-1", "root should be -1, got {r}");
}
}
#[test]
fn solve_quadratic_large_discriminant() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 - 100 * x + 1);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 2, "expected 2 roots");
verify_roots_are_zeros(&poly, &x, &roots, "x²-100x+1");
}
#[test]
fn solve_cubic_all_rational_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 3 - 2 * x ^ 2 - 5 * x + 6);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 3, "expected 3 roots");
verify_roots_are_zeros(&poly, &x, &roots, "x³-2x²-5x+6");
let strs: Vec<String> = roots.iter().map(|r| format!("{r}")).collect();
assert!(strs.contains(&"1".to_string()), "missing root 1: {strs:?}");
assert!(
strs.contains(&"-2".to_string()),
"missing root -2: {strs:?}"
);
assert!(strs.contains(&"3".to_string()), "missing root 3: {strs:?}");
}
#[test]
fn solve_cubic_one_rational_two_irrational_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 3 - 3 * x + 2);
let roots = poly.solve_or_empty(&x);
assert!(!roots.is_empty(), "expected roots for x³-3x+2");
verify_roots_are_zeros(&poly, &x, &roots, "x³-3x+2");
}
#[test]
fn solve_cubic_irrational_roots_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 3 - 2);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 3, "expected 3 roots for x³-2");
verify_roots_are_zeros(&poly, &x, &roots, "x³-2");
}
#[test]
fn solve_cubic_triple_root_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 3 - 6 * x ^ 2 + 12 * x - 8);
let roots = poly.solve_or_empty(&x);
assert!(!roots.is_empty(), "expected root for (x-2)³");
verify_roots_are_zeros(&poly, &x, &roots, "(x-2)³");
for r in &roots {
assert_eq!(format!("{r}"), "2", "triple root should be 2, got {r}");
}
}
#[test]
fn solve_depressed_cubic_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 3 + 3 * x - 4);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 3, "expected 3 roots");
verify_roots_are_zeros(&poly, &x, &roots, "x³+3x-4");
let strs: Vec<String> = roots.iter().map(|r| format!("{r}")).collect();
assert!(strs.contains(&"1".to_string()), "missing root 1: {strs:?}");
}
#[test]
fn solve_quartic_four_rational_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 4 - 5 * x ^ 2 + 4);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 4, "expected 4 roots");
verify_roots_are_zeros(&poly, &x, &roots, "x⁴-5x²+4");
let strs: Vec<String> = roots.iter().map(|r| format!("{r}")).collect();
for expected in &["1", "-1", "2", "-2"] {
assert!(
strs.contains(&expected.to_string()),
"missing root {expected}: {strs:?}"
);
}
}
#[test]
fn solve_quartic_two_real_two_complex_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 4 - 1);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 4, "expected 4 roots for x⁴-1");
verify_roots_are_zeros(&poly, &x, &roots, "x⁴-1");
}
#[test]
fn solve_quartic_all_complex_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 4 + 1);
let roots = poly.solve_or_empty(&x);
if roots.len() == 4 {
verify_roots_are_zeros(&poly, &x, &roots, "x⁴+1");
}
}
#[test]
fn solve_quartic_repeated_roots_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 4 - 2 * x ^ 2 + 1);
let roots = poly.solve_or_empty(&x);
assert!(!roots.is_empty(), "expected roots for (x-1)²(x+1)²");
verify_roots_are_zeros(&poly, &x, &roots, "(x-1)²(x+1)²");
}
#[test]
fn solve_quartic_with_rational_coeffs_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let built = (&x * 2 - 1) * (&x - 1) * (&x + 1) * (&x - 3);
let poly = built.expand().eval();
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 4, "expected 4 roots, got {}", roots.len());
verify_roots_are_zeros(&poly, &x, &roots, "(2x-1)(x-1)(x+1)(x-3)");
}
#[test]
fn factor_x4_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 4 - 1);
let factored = poly.factor(&x);
let s = format!("{factored}");
assert!(!s.contains("x^4"), "x⁴-1 should be factored, but got: {s}");
assert_equal_at_points(&poly, &factored, &x, "factor(x⁴-1)");
}
#[test]
fn factor_x2_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 - 1);
let factored = poly.factor(&x);
assert_equal_at_points(&poly, &factored, &x, "factor(x²-1)");
let s = format!("{factored}");
assert!(
!s.contains("x^2"),
"x²-1 should be factored to (x-1)(x+1), got: {s}"
);
}
#[test]
fn factor_x3_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 3 - 1);
let factored = poly.factor(&x);
assert_equal_at_points(&poly, &factored, &x, "factor(x³-1)");
let s = format!("{factored}");
assert!(!s.contains("x^3"), "x³-1 should be factored, got: {s}");
}
#[test]
fn factor_x6_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 6 - 1);
let factored = poly.factor(&x);
assert_equal_at_points(&poly, &factored, &x, "factor(x⁶-1)");
}
#[test]
fn factor_expand_roundtrip_quadratic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 + x - 6);
let factored = poly.factor(&x);
let re_expanded = factored.expand().eval();
assert_equal_at_points(&poly, &re_expanded, &x, "expand(factor(x²+x-6))");
}
#[test]
fn factor_expand_roundtrip_cubic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 3 - 6 * x ^ 2 + 11 * x - 6);
let factored = poly.factor(&x);
let re_expanded = factored.expand().eval();
assert_equal_at_points(&poly, &re_expanded, &x, "expand(factor(x³-6x²+11x-6))");
}
#[test]
fn factor_expand_roundtrip_quartic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 4 - 5 * x ^ 2 + 4);
let factored = poly.factor(&x);
let re_expanded = factored.expand().eval();
assert_equal_at_points(&poly, &re_expanded, &x, "expand(factor(x⁴-5x²+4))");
}
#[test]
fn factor_irreducible_stays_unchanged() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 + 1);
let factored = poly.factor(&x);
assert_equal_at_points(&poly, &factored, &x, "factor(x²+1)");
}
#[test]
fn factor_with_content() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, 4 * x ^ 2 - 4);
let factored = poly.factor(&x);
assert_equal_at_points(&poly, &factored, &x, "factor(4x²-4)");
}
#[test]
fn factor_perfect_square() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 + 6 * x + 9);
let factored = poly.factor(&x);
assert_equal_at_points(&poly, &factored, &x, "factor(x²+6x+9)");
}
#[test]
fn factor_difference_of_cubes() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 3 - 8);
let factored = poly.factor(&x);
assert_equal_at_points(&poly, &factored, &x, "factor(x³-8)");
let s = format!("{factored}");
assert!(!s.contains("x^3"), "x³-8 should be factored, got: {s}");
}
#[test]
fn factor_sum_of_cubes() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 3 + 8);
let factored = poly.factor(&x);
assert_equal_at_points(&poly, &factored, &x, "factor(x³+8)");
let s = format!("{factored}");
assert!(!s.contains("x^3"), "x³+8 should be factored, got: {s}");
}
#[test]
fn simplify_pythagorean_identity() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin().powi(2) + &x.cos().powi(2);
let simplified = expr.simplify();
assert_eq!(
format!("{simplified}"),
"1",
"sin²(x)+cos²(x) should simplify to 1"
);
}
#[test]
fn simplify_pythagorean_in_sum() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin().powi(2) + &x.cos().powi(2) + 2;
let simplified = expr.simplify();
assert_eq!(
format!("{simplified}"),
"3",
"2+sin²(x)+cos²(x) should simplify to 3"
);
}
#[test]
fn simplify_pythagorean_with_symbol() {
let ctx = Context::new();
let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
let expr = &y + &x.sin().powi(2) + &x.cos().powi(2);
let simplified = expr.simplify();
assert_eq!(
format!("{simplified}"),
"y + 1",
"y+sin²(x)+cos²(x) should simplify to y+1"
);
}
#[test]
fn simplify_exp_ln_composition() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.ln().exp();
let simplified = expr.simplify();
assert_eq!(
format!("{simplified}"),
"x",
"exp(ln(x)) should simplify to x"
);
}
#[test]
fn simplify_algebraic_fraction_x2_minus_1_over_x_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (x.powi(2) - 1) / (&x - 1);
let cancelled = expr.cancel(&x);
assert_eq!(
format!("{cancelled}"),
"x + 1",
"(x²-1)/(x-1) should cancel to x+1"
);
}
#[test]
fn simplify_expand_difference_of_squares() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x + 1) * (&x - 1);
let expanded = expr.expand().eval();
let _s = format!("{expanded}");
for &pt in &[-3i64, -1, 0, 1, 3] {
let v_expanded = eval_at(&expanded, &x, pt);
let v_expected = (pt * pt - 1) as f64;
assert!(
approx(v_expanded, v_expected, 1e-10),
"(x+1)(x-1) expanded at x={pt}: got {v_expanded}, expected {v_expected}"
);
}
}
#[test]
fn simplify_nested_square_minus_identity() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &(&x + 1).powi(2) - &x.powi(2) - &x * 2;
let simplified = expr.simplify();
assert_eq!(
format!("{simplified}"),
"1",
"(x+1)²-x²-2x should simplify to 1"
);
}
#[test]
fn simplify_double_negative() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = -(-&x);
let simplified = expr.simplify();
assert_eq!(format!("{simplified}"), "x", "-(-x) should simplify to x");
}
#[test]
fn simplify_zero_times_anything() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &ctx.int(0) * &(&x.powi(2) + &x.sin() + 17);
let simplified = expr.simplify();
assert!(
simplified.is_zero_structural(),
"0 * expr should be 0, got {simplified}"
);
}
#[test]
fn simplify_x_over_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x / &x;
let simplified = expr.simplify();
assert_eq!(format!("{simplified}"), "1", "x/x should simplify to 1");
}
#[test]
fn simplify_x_minus_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x - &x;
assert!(
expr.is_zero_structural() || expr.simplify().is_zero_structural(),
"x - x should be 0, got {expr}"
);
}
#[test]
fn simplify_power_of_power() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.powi(2).powi(3);
let simplified = expr.simplify();
for &pt in &[2i64, 3, -2] {
let v = eval_at(&simplified, &x, pt);
let expected = (pt as f64).powi(6);
assert!(
approx(v, expected, 1e-9),
"(x²)³ at x={pt}: got {v}, expected {expected}"
);
}
}
#[test]
fn simplify_sqrt_of_square_is_abs() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.powi(2).sqrt();
let simplified = expr.simplify();
assert_eq!(format!("{simplified}"), "abs(x)", "√(x²) should be |x|");
}
#[test]
fn subs_basic_polynomial() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = expr!(ctx, x ^ 2 + 3 * x + 2);
let result = f.subs_i64(&x, 5).eval();
assert_eq!(format!("{result}"), "42", "f(5) = 25+15+2 = 42");
}
#[test]
fn subs_symbolic() {
let ctx = Context::new();
let (x, a, b) = (ctx.symbol("x"), ctx.symbol("a"), ctx.symbol("b"));
let f = x.powi(2);
let result = f.subs(&x, &(&a + &b)).expand().eval();
let numerical = result.subs_i64(&a, 2).subs_i64(&b, 3).eval();
let v = numerical.eval_f64().expect("should eval");
assert!(
approx(v, 25.0, 1e-10),
"f(a+b) at a=2,b=3 should be 25, got {v}"
);
}
#[test]
fn subs_nested() {
let ctx = Context::new();
let (x, y, z) = (ctx.symbol("x"), ctx.symbol("y"), ctx.symbol("z"));
let f = x.powi(3);
let step1 = f.subs(&x, &(&y + 1));
let step2 = step1.subs(&y, &(&z + 2));
let val = step2.subs_i64(&z, 0).eval();
let v = val.eval_f64().expect("should eval");
assert!(
approx(v, 27.0, 1e-10),
"((z+2)+1)³ at z=0 should be 27, got {v}"
);
}
#[test]
fn subs_trig_argument() {
let ctx = Context::new();
let (x, _a) = (ctx.symbol("x"), ctx.symbol("a"));
let expr = x.sin();
let result = expr.subs(&x, &(&ctx.pi() / 6)).eval();
assert_eq!(format!("{result}"), "1/2", "sin(π/6) should be 1/2");
}
#[test]
fn subs_preserves_identity() {
let ctx = Context::new();
let (x, _y) = (ctx.symbol("x"), ctx.symbol("y"));
let f = expr!(ctx, x ^ 2 + x + 1);
let result = f.subs(&x, &x);
for &pt in &[-2i64, 0, 1, 3] {
let v_orig = eval_at(&f, &x, pt);
let v_sub = eval_at(&result, &x, pt);
assert!(
approx(v_orig, v_sub, 1e-10),
"subs(x,x) should be identity at x={pt}"
);
}
}
#[test]
fn subs_into_sum() {
let ctx = Context::new();
let (x, a, b) = (ctx.symbol("x"), ctx.symbol("a"), ctx.symbol("b"));
let f = expr!(ctx, x ^ 2 + 2 * x + 1);
let result = f.subs(&x, &(&a + &b)).expand().eval();
let v = result
.subs_i64(&a, 1)
.subs_i64(&b, 2)
.eval()
.eval_f64()
.expect("eval");
assert!(approx(v, 16.0, 1e-10), "f(1+2) should be 16, got {v}");
}
#[test]
fn cancel_basic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (x.powi(2) - 1) / (&x - 1);
let cancelled = expr.cancel(&x);
assert_eq!(
format!("{cancelled}"),
"x + 1",
"cancel((x²-1)/(x-1)) should give x+1"
);
}
#[test]
fn cancel_cubic_over_linear() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x.powi(3) - &x) / (&x + 1);
let cancelled = expr.cancel(&x);
for &pt in &[-3i64, -2, 0, 2, 3, 5] {
let v = eval_at(&cancelled, &x, pt);
let expected = (pt * pt - pt) as f64;
assert!(
approx(v, expected, 1e-9),
"cancel((x³-x)/(x+1)) at x={pt}: got {v}, expected {expected}"
);
}
}
#[test]
fn cancel_common_quadratic_factor() {
let ctx = Context::new();
let x = ctx.symbol("x");
let numer = (&x.powi(2) - 1) * (&x + 2);
let denom = x.powi(2) - 1;
let expr = &numer / &denom;
let cancelled = expr.cancel(&x);
for &pt in &[-3i64, 0, 2, 3, 5] {
let v = eval_at(&cancelled, &x, pt);
let expected = (pt + 2) as f64;
assert!(
approx(v, expected, 1e-9),
"cancel at x={pt}: got {v}, expected {expected}"
);
}
}
#[test]
fn cancel_no_common_factor() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x + 1) / (&x + 2);
let cancelled = expr.cancel(&x);
for &pt in &[-3i64, 0, 1, 3, 5] {
let v_orig = eval_at(&expr, &x, pt);
let v_canc = eval_at(&cancelled, &x, pt);
assert!(
approx(v_orig, v_canc, 1e-9),
"cancel should not change (x+1)/(x+2) at x={pt}"
);
}
}
#[test]
fn poly_gcd_basic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = expr!(ctx, x ^ 2 - 1);
let b = expr!(ctx, x ^ 2 - 2 * x + 1);
let g = a.poly_gcd(&b, &x).expect("a.poly_gcd(&b, &x) must be Some");
let v1 = eval_at(&g, &x, 1);
assert!(
approx(v1, 0.0, 1e-9),
"gcd(x²-1, (x-1)²) should vanish at x=1, got {v1}"
);
let vm1 = eval_at(&g, &x, -1);
assert!(vm1.abs() > 0.1, "gcd should be nonzero at x=-1, got {vm1}");
}
#[test]
fn poly_gcd_coprime() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = &x + 1;
let b = &x + 2;
let g = a.poly_gcd(&b, &x).expect("a.poly_gcd(&b, &x) must be Some");
let v0 = eval_at(&g, &x, 0);
let v5 = eval_at(&g, &x, 5);
assert!(
approx(v0, v5, 1e-9),
"gcd(x+1,x+2) should be constant, but varies: {v0} vs {v5}"
);
}
#[test]
fn cancel_perfect_square_over_root() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (x.powi(2) + &x * 2 + 1) / (&x + 1);
let cancelled = expr.cancel(&x);
assert_eq!(
format!("{cancelled}"),
"x + 1",
"(x²+2x+1)/(x+1) should cancel to x+1"
);
}
#[test]
fn system_circle_line() {
let ctx = Context::new();
symplex::syms!(ctx; x, y);
let eq1 = expr!(ctx, x ^ 2 + y ^ 2 - 1);
let eq2 = expr!(ctx, x + y - 1);
let solutions =
symplex::polysys::solve_system_ex(&[eq1.clone(), eq2.clone()], &[x.clone(), y.clone()])
.unwrap();
assert_eq!(solutions.len(), 2, "expected 2 solutions");
for sol in &solutions {
let r1 = eq1.subs(&x, &sol[0]).subs(&y, &sol[1]).eval().simplify();
let r2 = eq2.subs(&x, &sol[0]).subs(&y, &sol[1]).eval().simplify();
assert!(
r1.is_zero_structural(),
"eq1 residual should be 0, got {r1}"
);
assert!(
r2.is_zero_structural(),
"eq2 residual should be 0, got {r2}"
);
}
}
#[test]
fn system_two_conics() {
let ctx = Context::new();
symplex::syms!(ctx; x, y);
let eq1 = expr!(ctx, x ^ 2 + y ^ 2 - 5);
let eq2 = expr!(ctx, x * y - 2);
let solutions =
symplex::polysys::solve_system_ex(&[eq1.clone(), eq2.clone()], &[x.clone(), y.clone()])
.unwrap();
assert_eq!(solutions.len(), 4, "expected 4 solutions");
for sol in &solutions {
let r1 = eq1.subs(&x, &sol[0]).subs(&y, &sol[1]).eval().simplify();
let r2 = eq2.subs(&x, &sol[0]).subs(&y, &sol[1]).eval().simplify();
assert!(
r1.is_zero_structural(),
"eq1 not satisfied for ({}, {}): residual = {r1}",
sol[0],
sol[1]
);
assert!(
r2.is_zero_structural(),
"eq2 not satisfied for ({}, {}): residual = {r2}",
sol[0],
sol[1]
);
}
}
#[test]
fn system_linear_2x2() {
let ctx = Context::new();
symplex::syms!(ctx; x, y);
let eq1 = expr!(ctx, x + y - 5);
let eq2 = expr!(ctx, x - y - 1);
let solutions =
symplex::polysys::solve_system_ex(&[eq1.clone(), eq2.clone()], &[x.clone(), y.clone()])
.unwrap();
assert_eq!(solutions.len(), 1, "expected 1 solution");
assert_eq!(format!("{}", solutions[0][0]), "3", "x should be 3");
assert_eq!(format!("{}", solutions[0][1]), "2", "y should be 2");
}
#[test]
fn system_parabola_line() {
let ctx = Context::new();
symplex::syms!(ctx; x, y);
let eq1 = expr!(ctx, y - x ^ 2);
let eq2 = expr!(ctx, y - x - 2);
let solutions =
symplex::polysys::solve_system_ex(&[eq1.clone(), eq2.clone()], &[x.clone(), y.clone()])
.unwrap();
assert_eq!(solutions.len(), 2, "expected 2 solutions");
for sol in &solutions {
let r1 = eq1.subs(&x, &sol[0]).subs(&y, &sol[1]).eval().simplify();
let r2 = eq2.subs(&x, &sol[0]).subs(&y, &sol[1]).eval().simplify();
assert!(
r1.is_zero_structural(),
"y-x² not satisfied: residual {r1} at ({}, {})",
sol[0],
sol[1]
);
assert!(
r2.is_zero_structural(),
"y-x-2 not satisfied: residual {r2} at ({}, {})",
sol[0],
sol[1]
);
}
}
#[test]
fn expand_binomial_power() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x + 1).powi(3);
let expanded = expr.expand().eval();
for &pt in &[-2i64, -1, 0, 1, 2, 3] {
let v = eval_at(&expanded, &x, pt);
let expected = ((pt + 1) as f64).powi(3);
assert!(
approx(v, expected, 1e-9),
"(x+1)³ expanded at x={pt}: got {v}, expected {expected}"
);
}
}
#[test]
fn expand_product_of_sums() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x + 1) * (&x + 2) * (&x + 3);
let expanded = expr.expand().eval();
for &pt in &[-4i64, -3, -2, -1, 0, 1, 2] {
let v = eval_at(&expanded, &x, pt);
let expected = ((pt + 1) * (pt + 2) * (pt + 3)) as f64;
assert!(
approx(v, expected, 1e-9),
"(x+1)(x+2)(x+3) expanded at x={pt}: got {v}, expected {expected}"
);
}
}
#[test]
fn expand_fourth_power() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x + 1).powi(4);
let expanded = expr.expand().eval();
for &pt in &[-2i64, -1, 0, 1, 2, 3] {
let v = eval_at(&expanded, &x, pt);
let expected = ((pt + 1) as f64).powi(4);
assert!(
approx(v, expected, 1e-9),
"(x+1)⁴ expanded at x={pt}: got {v}, expected {expected}"
);
}
}
#[test]
fn partial_fractions_basic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = ctx.int(1) / (x.powi(2) - 1);
let pf = expr.partial_fractions(&x);
for &pt in &[-3i64, -2, 0, 2, 3, 5] {
let v_orig = eval_at(&expr, &x, pt);
let v_pf = eval_at(&pf, &x, pt);
assert!(
approx(v_orig, v_pf, 1e-9),
"partial_fractions(1/(x²-1)) at x={pt}: orig={v_orig}, pf={v_pf}"
);
}
}
#[test]
fn partial_fractions_cubic_denom() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = ctx.int(1) / (&x.powi(3) - &x);
let pf = expr.partial_fractions(&x);
for &pt in &[-3i64, -2, 2, 3, 5, 7] {
let v_orig = eval_at(&expr, &x, pt);
let v_pf = eval_at(&pf, &x, pt);
assert!(
approx(v_orig, v_pf, 1e-9),
"partial_fractions(1/(x³-x)) at x={pt}: orig={v_orig}, pf={v_pf}"
);
}
}
#[test]
fn degree_of_polynomial() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(expr!(ctx, x ^ 3 + 2 * x + 1).degree(&x), Some(3));
assert_eq!(expr!(ctx, x ^ 2 - 1).degree(&x), Some(2));
assert_eq!((&x + 1).degree(&x), Some(1));
assert_eq!(ctx.int(5).degree(&x), Some(0));
}
#[test]
fn coefficients_extraction() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 - 5 * x + 6);
let coeffs = poly.coeffs(&x).expect("poly.coeffs(&x) must be Some");
assert_eq!(coeffs.len(), 3, "quadratic should have 3 coefficients");
assert_eq!(format!("{}", coeffs[0]), "6", "constant term should be 6");
assert_eq!(
format!("{}", coeffs[1]),
"-5",
"linear coefficient should be -5"
);
assert_eq!(
format!("{}", coeffs[2]),
"1",
"leading coefficient should be 1"
);
}
#[test]
fn eval_sin_special_values() {
let ctx = Context::new();
let v = ctx.int(0).sin().eval();
assert_eq!(format!("{v}"), "0", "sin(0) should be 0");
let v = (&ctx.pi() / 2).sin().eval();
assert_eq!(format!("{v}"), "1", "sin(π/2) should be 1");
let v = ctx.pi().sin().eval();
assert_eq!(format!("{v}"), "0", "sin(π) should be 0");
let v = (&ctx.pi() / 6).sin().eval();
assert_eq!(format!("{v}"), "1/2", "sin(π/6) should be 1/2");
}
#[test]
fn eval_cos_special_values() {
let ctx = Context::new();
let v = ctx.int(0).cos().eval();
assert_eq!(format!("{v}"), "1", "cos(0) should be 1");
let v = (&ctx.pi() / 3).cos().eval();
assert_eq!(format!("{v}"), "1/2", "cos(π/3) should be 1/2");
let v = ctx.pi().cos().eval();
assert_eq!(format!("{v}"), "-1", "cos(π) should be -1");
}
#[test]
fn solve_and_factor_agree_on_roots() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 - 5 * x + 6);
let roots = poly.solve_or_empty(&x);
let factored = poly.factor(&x);
for root in &roots {
let v = factored.subs(&x, root).eval().simplify();
assert!(
v.is_zero_structural() || v.eval_f64().map(|f| f.abs() < 1e-10).unwrap_or(false),
"root {root} from solve is not a root of factored form, residual = {v}"
);
}
}
#[test]
fn factor_then_expand_is_identity() {
let ctx = Context::new();
let x = ctx.symbol("x");
let polys = vec![
expr!(ctx, x ^ 2 - 1),
expr!(ctx, x ^ 3 - 6 * x ^ 2 + 11 * x - 6),
expr!(ctx, x ^ 4 - 5 * x ^ 2 + 4),
expr!(ctx, x ^ 3 - x),
];
for poly in &polys {
let factored = poly.factor(&x);
let re_expanded = factored.expand().eval();
assert_equal_at_points(
poly,
&re_expanded,
&x,
&format!("factor/expand roundtrip for {poly}"),
);
}
}
#[test]
fn cancel_and_simplify_agree() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (x.powi(2) - 4) / (&x - 2);
let cancelled = expr.cancel(&x);
let v = eval_at(&cancelled, &x, 5);
assert!(
approx(v, 7.0, 1e-10),
"cancel((x²-4)/(x-2)) at x=5 should be 7, got {v}"
);
}
#[test]
fn solve_linear_trivial() {
let ctx = Context::new();
let x = ctx.symbol("x");
let roots = x.solve_or_empty(&x);
assert_eq!(roots.len(), 1);
assert_eq!(format!("{}", roots[0]), "0");
}
#[test]
fn solve_already_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let roots = ctx.int(0).solve_or_empty(&x);
assert!(
roots.is_empty(),
"0=0 should return empty (infinitely many solutions)"
);
}
#[test]
fn solve_constant_nonzero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let roots = ctx.int(7).solve_or_empty(&x);
assert!(roots.is_empty(), "7=0 should have no solutions");
}
#[test]
fn expand_then_factor_preserves_value_for_x5_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 5 - 1);
let factored = poly.factor(&x);
let re_expanded = factored.expand().eval();
assert_equal_at_points(&poly, &re_expanded, &x, "x⁵-1 factor/expand");
}
#[test]
fn check_solution_api() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 - 4);
assert_eq!(
poly.check_solution(&x, &ctx.int(2)),
Some(true),
"x=2 should satisfy x²-4=0"
);
assert_eq!(
poly.check_solution(&x, &ctx.int(-2)),
Some(true),
"x=-2 should satisfy x²-4=0"
);
assert_eq!(
poly.check_solution(&x, &ctx.int(3)),
Some(false),
"x=3 should NOT satisfy x²-4=0"
);
}
#[test]
fn solve_quartic_vieta_sum_of_roots() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 4 - 10 * x ^ 3 + 35 * x ^ 2 - 50 * x + 24);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 4, "expected 4 roots");
let sum: f64 = roots
.iter()
.map(|r| r.eval_f64().expect("root should eval"))
.sum();
assert!(
approx(sum, 10.0, 1e-9),
"sum of roots should be 10 (Vieta's), got {sum}"
);
let product: f64 = roots
.iter()
.map(|r| r.eval_f64().expect("root should eval"))
.product();
assert!(
approx(product, 24.0, 1e-9),
"product of roots should be 24 (Vieta's), got {product}"
);
}
#[test]
fn solve_cubic_vieta_relations() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 3 - 6 * x ^ 2 + 11 * x - 6);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 3, "expected 3 roots");
let vals: Vec<f64> = roots
.iter()
.map(|r| r.eval_f64().expect("root should eval"))
.collect();
let sum: f64 = vals.iter().sum();
assert!(
approx(sum, 6.0, 1e-9),
"sum of roots should be 6, got {sum}"
);
let product: f64 = vals.iter().product();
assert!(
approx(product, 6.0, 1e-9),
"product of roots should be 6, got {product}"
);
let pair_sum = vals[0] * vals[1] + vals[0] * vals[2] + vals[1] * vals[2];
assert!(
approx(pair_sum, 11.0, 1e-9),
"sum of pairwise products should be 11, got {pair_sum}"
);
}
#[test]
fn factor_x4_plus_4() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 4 + 4);
let factored = poly.factor(&x);
assert_equal_at_points(&poly, &factored, &x, "factor(x⁴+4)");
let s = format!("{factored}");
assert!(
!s.contains("x^4"),
"x⁴+4 should factor (Sophie Germain), got: {s}"
);
}
#[test]
fn factor_x8_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 8 - 1);
let factored = poly.factor(&x);
assert_equal_at_points(&poly, &factored, &x, "factor(x⁸-1)");
}
#[test]
fn cancel_higher_degree_common_factor() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x.powi(3) - &x) / (&x.powi(2) - 1);
let cancelled = expr.cancel(&x);
for &pt in &[-3i64, -2, 0, 2, 3, 5] {
let v = eval_at(&cancelled, &x, pt);
assert!(
approx(v, pt as f64, 1e-9),
"(x³-x)/(x²-1) should cancel to x, got {v} at x={pt}"
);
}
}
#[test]
fn simplify_sin_squared_times_two_plus_cos_squared_times_two() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin().powi(2) * 2 + &x.cos().powi(2) * 2;
let simplified = expr.simplify();
let s = format!("{simplified}");
let full = expr.simplify();
let full_s = format!("{full}");
for &pt in &[-3i64, -1, 0, 1, 2, 5] {
let v = eval_at(&simplified, &x, pt);
assert!(
approx(v, 2.0, 1e-9),
"2sin²(x)+2cos²(x) should always be 2, got {v} at x={pt}"
);
}
assert!(
s == "2" || full_s == "2",
"BUG: 2*sin²(x)+2*cos²(x) should simplify to 2, \
but simplify gave '{s}' and full_simplify gave '{full_s}'"
);
}
#[test]
fn simplify_sin_squared_times_three_plus_cos_squared_times_three() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin().powi(2) * 3 + &x.cos().powi(2) * 3;
let simplified = expr.simplify();
let full = expr.simplify();
let s = format!("{simplified}");
let full_s = format!("{full}");
for &pt in &[-2i64, 0, 1, 4] {
let v = eval_at(&simplified, &x, pt);
assert!(
approx(v, 3.0, 1e-9),
"3sin²+3cos² should be 3 at x={pt}, got {v}"
);
}
assert!(
s == "3" || full_s == "3",
"BUG: 3*sin²(x)+3*cos²(x) should simplify to 3, \
but simplify gave '{s}' and full_simplify gave '{full_s}'"
);
}
#[test]
fn simplify_y_times_sin_squared_plus_y_times_cos_squared() {
let ctx = Context::new();
let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
let expr = &y * &x.sin().powi(2) + &y * &x.cos().powi(2);
let simplified = expr.simplify();
let full = expr.simplify();
let s = format!("{simplified}");
let full_s = format!("{full}");
assert!(
s == "y" || full_s == "y",
"BUG: y*sin²(x)+y*cos²(x) should simplify to y, \
but simplify gave '{s}' and full_simplify gave '{full_s}'"
);
}
#[test]
fn simplify_half_sin_squared_plus_half_cos_squared() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin().powi(2) / 2 + &x.cos().powi(2) / 2;
let simplified = expr.simplify();
let full = expr.simplify();
let s = format!("{simplified}");
let full_s = format!("{full}");
for &pt in &[-1i64, 0, 1, 3] {
let v = eval_at(&simplified, &x, pt);
assert!(
approx(v, 0.5, 1e-9),
"sin²/2+cos²/2 should be 0.5 at x={pt}, got {v}"
);
}
assert!(
s == "1/2" || full_s == "1/2",
"BUG: sin²(x)/2+cos²(x)/2 should simplify to 1/2, \
but simplify gave '{s}' and full_simplify gave '{full_s}'"
);
}
#[test]
fn solve_quadratic_with_fraction_coefficients() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = &x.powi(2) / 2 - &x * ctx.rational(3, 2) + 1;
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 2, "expected 2 roots");
verify_roots_are_zeros(&poly, &x, &roots, "(1/2)x²-(3/2)x+1");
}
#[test]
fn expand_large_binomial() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x + 1).powi(6);
let expanded = expr.expand().eval();
let v = eval_at(&expanded, &x, 1);
assert!(
approx(v, 64.0, 1e-9),
"(x+1)^6 at x=1 should be 64, got {v}"
);
let v = eval_at(&expanded, &x, 2);
assert!(
approx(v, 729.0, 1e-9),
"(x+1)^6 at x=2 should be 729, got {v}"
);
}
#[test]
fn solve_quintic_with_rational_root() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 5 - x);
let roots = poly.solve_or_empty(&x);
assert!(
roots.len() >= 3,
"x⁵-x should have at least 3 roots (0, ±1, ±i), got {}",
roots.len()
);
verify_roots_are_zeros(&poly, &x, &roots, "x⁵-x");
}
#[test]
fn subs_chain_consistency() {
let ctx = Context::new();
let (x, a) = (ctx.symbol("x"), ctx.symbol("a"));
let f = expr!(ctx, x ^ 3 - 2 * x + 1);
let fa = f.subs(&x, &a);
let v_orig = eval_at(&f, &x, 3); let v_sub = eval_at(&fa, &a, 3);
assert!(
approx(v_orig, v_sub, 1e-10),
"f(3) should equal f(x).subs(x,a) evaluated at a=3: {v_orig} vs {v_sub}"
);
}
#[test]
fn differentiate_then_solve_for_critical_points() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = expr!(ctx, x ^ 3 - 3 * x + 2);
let df = f.diff(&x);
let critical = df.solve_or_empty(&x);
assert_eq!(critical.len(), 2, "f'(x)=3x²-3 should have 2 roots");
let strs: Vec<String> = critical.iter().map(|r| format!("{r}")).collect();
assert!(
strs.contains(&"1".to_string()),
"missing critical point 1: {strs:?}"
);
assert!(
strs.contains(&"-1".to_string()),
"missing critical point -1: {strs:?}"
);
}
#[test]
fn cancel_x3_minus_x2_over_x2_minus_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x.powi(3) - &x.powi(2)) / (&x.powi(2) - &x);
let cancelled = expr.cancel(&x);
for &pt in &[2i64, 3, 5, -2, -3] {
let v = eval_at(&cancelled, &x, pt);
assert!(
approx(v, pt as f64, 1e-9),
"(x³-x²)/(x²-x) should cancel to x at x={pt}, got {v}"
);
}
}
#[test]
fn solve_quadratic_negative_discriminant_gives_complex_roots() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 2 + x + 1);
let roots = poly.solve_or_empty(&x);
assert_eq!(roots.len(), 2, "x²+x+1 should have 2 complex roots");
verify_roots_are_zeros(&poly, &x, &roots, "x²+x+1");
}
#[test]
fn system_tangent_circles() {
let ctx = Context::new();
symplex::syms!(ctx; x, y);
let eq1 = expr!(ctx, x ^ 2 + y ^ 2 - 4);
let eq2 = expr!(ctx, (x - 3) ^ 2 + y ^ 2 - 1);
let eq2_expanded = eq2.expand().eval();
let solutions = symplex::polysys::solve_system_ex(
&[eq1.clone(), eq2_expanded.clone()],
&[x.clone(), y.clone()],
)
.unwrap();
assert_eq!(solutions.len(), 1, "tangent circles should have 1 solution");
let r1 = eq1
.subs(&x, &solutions[0][0])
.subs(&y, &solutions[0][1])
.eval()
.simplify();
assert!(
r1.is_zero_structural(),
"solution should satisfy eq1, residual = {r1}"
);
}
#[test]
fn simplify_one_minus_sin_squared() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = ctx.int(1) - x.sin().powi(2);
let simplified = expr.simplify();
let s = format!("{simplified}");
for &pt in &[-2i64, -1, 0, 1, 2, 3] {
let v = eval_at(&simplified, &x, pt);
let pt_f = pt as f64;
let expected = pt_f.cos().powi(2);
assert!(
approx(v, expected, 1e-9),
"1-sin²(x) at x={pt}: got {v}, expected {expected}"
);
}
assert!(
s.contains("cos") || s.contains("sin") || s == "1",
"1-sin²(x) should simplify to cos²(x) or equivalent, got: {s}"
);
}
#[test]
fn expand_square_of_difference() {
let ctx = Context::new();
let (a, b) = (ctx.symbol("a"), ctx.symbol("b"));
let expr = (&a - &b).powi(2);
let expanded = expr.expand().eval();
let v = expanded
.subs_i64(&a, 5)
.subs_i64(&b, 3)
.eval()
.eval_f64()
.unwrap();
assert!(
approx(v, 4.0, 1e-10),
"(a-b)² at a=5,b=3 should be 4, got {v}"
);
let v = expanded
.subs_i64(&a, 1)
.subs_i64(&b, 4)
.eval()
.eval_f64()
.unwrap();
assert!(
approx(v, 9.0, 1e-10),
"(a-b)² at a=1,b=4 should be 9, got {v}"
);
}
#[test]
fn cancel_bivariate_difference_of_squares() {
let ctx = Context::new();
let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
let numer = &x.powi(2) - &y.powi(2);
let denom = &x - &y;
let expr = &numer / &denom;
let cancelled = expr.cancel(&x);
let v = cancelled
.subs_i64(&x, 5)
.subs_i64(&y, 2)
.eval()
.eval_f64()
.unwrap();
assert!(
approx(v, 7.0, 1e-10),
"(x²-y²)/(x-y) cancelled at x=5,y=2 should be 7, got {v}"
);
}
#[test]
fn solve_quartic_quadruple_root() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = expr!(ctx, x ^ 4 - 4 * x ^ 3 + 6 * x ^ 2 - 4 * x + 1);
let roots = poly.solve_or_empty(&x);
assert!(!roots.is_empty(), "(x-1)⁴ should have root(s)");
verify_roots_are_zeros(&poly, &x, &roots, "(x-1)⁴");
for r in &roots {
assert_eq!(format!("{r}"), "1", "quadruple root should be 1, got {r}");
}
}
#[test]
fn diff_then_solve_quartic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = expr!(ctx, x ^ 4 - 4 * x ^ 3);
let df = f.diff(&x);
let critical = df.solve_or_empty(&x);
let strs: Vec<String> = critical.iter().map(|r| format!("{r}")).collect();
assert!(
strs.contains(&"0".to_string()),
"missing critical point 0: {strs:?}"
);
assert!(
strs.contains(&"3".to_string()),
"missing critical point 3: {strs:?}"
);
verify_roots_are_zeros(&df, &x, &critical, "f'(x)=4x³-12x²");
}
#[test]
fn poly_gcd_shared_quadratic_factor() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = expr!(ctx, x ^ 3 + 3 * x ^ 2 - x - 3);
let b = expr!(ctx, x ^ 3 - 5 * x ^ 2 - x + 5);
let g = a.poly_gcd(&b, &x).expect("a.poly_gcd(&b, &x) must be Some");
let v1 = eval_at(&g, &x, 1);
assert!(approx(v1, 0.0, 1e-9), "gcd should vanish at x=1, got {v1}");
let vm1 = eval_at(&g, &x, -1);
assert!(
approx(vm1, 0.0, 1e-9),
"gcd should vanish at x=-1, got {vm1}"
);
let v2 = eval_at(&g, &x, 2);
assert!(v2.abs() > 0.1, "gcd should be nonzero at x=2, got {v2}");
if let Some(deg) = g.degree(&x) {
assert_eq!(deg, 2, "gcd degree should be 2, got {deg}");
}
}