use symplex::prelude::*;
fn assert_exact_at(orig: &Ex, pf: &Ex, x: &Ex, pt: &Ex, reference: f64) {
let o = orig
.subs(x, pt)
.eval_f64()
.unwrap_or_else(|err| panic!("{orig} at {pt} did not evaluate: {err}"));
let p = pf
.subs(x, pt)
.eval_f64()
.unwrap_or_else(|err| panic!("{pf} at {pt} did not evaluate: {err}"));
let scale = reference.abs();
assert!(
(o - reference).abs() <= 1e-15 * scale,
"input {orig} at {pt}: got {o}, sympy says {reference}"
);
assert!(
(p - reference).abs() <= 1e-15 * scale,
"partial_fractions at {pt}: got {p}, sympy says {reference} (relative error {:e})",
(p - reference).abs() / scale
);
}
fn longest_digit_run(s: &str) -> usize {
let mut best = 0;
let mut cur = 0;
for c in s.chars() {
if c.is_ascii_digit() {
cur += 1;
best = best.max(cur);
} else {
cur = 0;
}
}
best
}
#[test]
fn apart_irrational_cubic_poles_are_not_rounded_floats() {
let ctx = Context::new();
let x = ctx.symbol("x");
let e = ctx.int(1) / (ctx.rational(-91, 64) * x.powi(3) + ctx.int(3) * &x - ctx.int(1));
let pf = e.partial_fractions(&x);
let s = pf.to_string();
assert!(
longest_digit_run(&s) <= 6,
"partial_fractions emitted a float-rounded rational (integer literal > 10^6): {s}"
);
assert_exact_at(&e, &pf, &x, &ctx.int(3), -0.032904884318766064);
assert_exact_at(&e, &pf, &x, &ctx.rational(5, 2), -0.06362619609792469);
assert_exact_at(&e, &pf, &x, &ctx.int(-7), 0.002147290723033048);
}
#[test]
fn apart_x_cubed_minus_two_is_exact() {
let ctx = Context::new();
let x = ctx.symbol("x");
let e = ctx.int(1) / (x.powi(3) - ctx.int(2));
let pf = e.partial_fractions(&x);
let s = pf.to_string();
assert!(
longest_digit_run(&s) <= 6,
"partial_fractions emitted a float-rounded rational: {s}"
);
assert_exact_at(&e, &pf, &x, &ctx.int(3), 0.04);
assert_exact_at(&e, &pf, &x, &ctx.rational(5, 2), 0.07339449541284404);
}
#[test]
fn apart_difference_of_squares_still_decomposes() {
let ctx = Context::new();
let x = ctx.symbol("x");
let e = ctx.int(1) / (x.powi(2) - ctx.int(1));
let pf = e.partial_fractions(&x);
let expected = ctx.rational(1, 2) / (&x - ctx.int(1)) - ctx.rational(1, 2) / (&x + ctx.int(1));
assert_ne!(pf.id(), e.id(), "1/(x^2 - 1) should be decomposed");
assert_eq!(
pf.equals(&expected),
Some(true),
"got {pf}, sympy says {expected}"
);
}
#[test]
fn apart_with_zero_pole_still_decomposes() {
let ctx = Context::new();
let x = ctx.symbol("x");
let e = (&x + ctx.int(1)) / (x.powi(3) - &x);
let pf = e.partial_fractions(&x);
let expected = ctx.int(1) / (&x - ctx.int(1)) - ctx.int(1) / &x;
assert_ne!(pf.id(), e.id(), "(x + 1)/(x^3 - x) should be decomposed");
assert_eq!(
pf.equals(&expected),
Some(true),
"got {pf}, sympy says {expected}"
);
}
#[test]
fn apart_irreducible_quadratic_unchanged() {
let ctx = Context::new();
let x = ctx.symbol("x");
let e = ctx.int(1) / (x.powi(2) + ctx.int(1));
let pf = e.partial_fractions(&x);
assert_eq!(
pf.equals(&e),
Some(true),
"1/(x^2 + 1) should stay: got {pf}"
);
}
#[test]
fn apart_x4_plus_1_verified_sqrt2_factors_still_decompose() {
let ctx = Context::new();
let x = ctx.symbol("x");
let e = ctx.int(1) / (x.powi(4) + ctx.int(1));
let pf = e.partial_fractions(&x);
let s = pf.to_string();
assert_ne!(pf.id(), e.id(), "1/(x^4 + 1) should decompose over Q(√2)");
assert!(s.contains("sqrt(2)"), "expected √2 factors: {s}");
assert!(
longest_digit_run(&s) <= 6,
"partial_fractions emitted a float-rounded rational: {s}"
);
assert_exact_at(&e, &pf, &x, &ctx.int(3), 0.012195121951219513);
}