mathhook_core/calculus/integrals/rational/
helpers.rs

1//! Helper utilities for rational function integration
2
3use crate::core::{Expression, Number, Symbol};
4use crate::simplify::Simplify;
5
6/// Check if expression is a polynomial in the given variable
7///
8/// # Arguments
9///
10/// * `expr` - The expression to check
11/// * `var` - The variable
12///
13/// # Examples
14///
15/// ```
16/// use mathhook_core::{Expression, symbol};
17/// use mathhook_core::calculus::integrals::rational::helpers::is_polynomial;
18///
19/// let x = symbol!(x);
20/// let poly = Expression::add(vec![
21///     Expression::pow(Expression::symbol(x.clone()), Expression::integer(2)),
22///     Expression::symbol(x.clone()),
23///     Expression::integer(1),
24/// ]);
25///
26/// assert!(is_polynomial(&poly, &x));
27///
28/// let non_poly = Expression::function("sin", vec![Expression::symbol(x.clone())]);
29/// assert!(!is_polynomial(&non_poly, &x));
30/// ```
31pub fn is_polynomial(expr: &Expression, var: &Symbol) -> bool {
32    match expr {
33        Expression::Number(_) => true,
34        Expression::Symbol(s) => s == var || !expr.contains_variable(var),
35        Expression::Add(terms) => terms.iter().all(|t| is_polynomial(t, var)),
36        Expression::Mul(factors) => factors.iter().all(|f| is_polynomial(f, var)),
37        Expression::Pow(base, exp) => {
38            if let Expression::Symbol(s) = base.as_ref() {
39                if s == var {
40                    matches!(exp.as_ref(), Expression::Number(Number::Integer(n)) if *n >= 0)
41                } else {
42                    is_polynomial(base, var)
43                }
44            } else {
45                is_polynomial(base, var)
46                    && matches!(exp.as_ref(), Expression::Number(Number::Integer(n)) if *n >= 0)
47            }
48        }
49        _ => false,
50    }
51}
52
53/// Get polynomial degree with respect to a variable
54///
55/// # Arguments
56///
57/// * `expr` - The polynomial expression
58/// * `var` - The variable
59///
60/// # Examples
61///
62/// ```
63/// use mathhook_core::{Expression, symbol};
64/// use mathhook_core::calculus::integrals::rational::helpers::polynomial_degree;
65///
66/// let x = symbol!(x);
67/// let cubic = Expression::pow(Expression::symbol(x.clone()), Expression::integer(3));
68/// assert_eq!(polynomial_degree(&cubic, &x), 3);
69///
70/// let linear = Expression::symbol(x.clone());
71/// assert_eq!(polynomial_degree(&linear, &x), 1);
72///
73/// let constant = Expression::integer(5);
74/// assert_eq!(polynomial_degree(&constant, &x), 0);
75/// ```
76pub fn polynomial_degree(expr: &Expression, var: &Symbol) -> i64 {
77    match expr {
78        Expression::Symbol(s) if s == var => 1,
79        Expression::Number(_) => 0,
80        Expression::Pow(base, exp) => {
81            if let (Expression::Symbol(s), Expression::Number(Number::Integer(e))) =
82                (base.as_ref(), exp.as_ref())
83            {
84                if s == var {
85                    return *e;
86                }
87            }
88            0
89        }
90        Expression::Add(terms) => terms
91            .iter()
92            .map(|t| polynomial_degree(t, var))
93            .max()
94            .unwrap_or(0),
95        Expression::Mul(factors) => factors.iter().map(|f| polynomial_degree(f, var)).sum(),
96        _ => 0,
97    }
98}
99
100/// Substitute a value for a variable in an expression
101///
102/// # Arguments
103///
104/// * `expr` - The expression
105/// * `var` - The variable to substitute
106/// * `value` - The value to substitute
107///
108/// # Examples
109///
110/// ```
111/// use mathhook_core::{Expression, symbol};
112/// use mathhook_core::calculus::integrals::rational::helpers::substitute_variable;
113/// use mathhook_core::simplify::Simplify;
114///
115/// let x = symbol!(x);
116/// let expr = Expression::add(vec![
117///     Expression::pow(Expression::symbol(x.clone()), Expression::integer(2)),
118///     Expression::integer(1),
119/// ]);
120///
121/// let result = substitute_variable(&expr, &x, &Expression::integer(3));
122/// assert_eq!(result.simplify(), Expression::integer(10));
123/// ```
124pub fn substitute_variable(expr: &Expression, var: &Symbol, value: &Expression) -> Expression {
125    match expr {
126        Expression::Symbol(s) if s == var => value.clone(),
127        Expression::Add(terms) => Expression::add(
128            terms
129                .iter()
130                .map(|t| substitute_variable(t, var, value))
131                .collect(),
132        ),
133        Expression::Mul(factors) => Expression::mul(
134            factors
135                .iter()
136                .map(|f| substitute_variable(f, var, value))
137                .collect(),
138        ),
139        Expression::Pow(base, exp) => Expression::pow(
140            substitute_variable(base, var, value),
141            substitute_variable(exp, var, value),
142        ),
143        Expression::Function { name, args } => Expression::function(
144            name,
145            args.iter()
146                .map(|a| substitute_variable(a, var, value))
147                .collect(),
148        ),
149        _ => expr.clone(),
150    }
151}
152
153/// Compute factorial of a non-negative integer
154///
155/// # Arguments
156///
157/// * `n` - Non-negative integer
158///
159/// # Examples
160///
161/// ```
162/// use mathhook_core::calculus::integrals::rational::helpers::factorial;
163///
164/// assert_eq!(factorial(0), 1);
165/// assert_eq!(factorial(1), 1);
166/// assert_eq!(factorial(5), 120);
167/// assert_eq!(factorial(10), 3628800);
168/// ```
169pub fn factorial(n: i64) -> i64 {
170    if n <= 0 {
171        1
172    } else {
173        (1..=n).product()
174    }
175}
176
177/// Try to extract quadratic coefficients from x² + px + q
178///
179/// Returns Some((p, q)) if expression matches x² + px + q pattern where the
180/// coefficient of x² is exactly 1.
181///
182/// # Arguments
183///
184/// * `expr` - The expression to analyze
185/// * `var` - The variable to match against
186///
187/// # Examples
188///
189/// ```
190/// use mathhook_core::{Expression, symbol};
191/// use mathhook_core::calculus::integrals::rational::helpers::try_extract_quadratic;
192///
193/// let x = symbol!(x);
194///
195/// let quadratic = Expression::add(vec![
196///     Expression::pow(Expression::symbol(x.clone()), Expression::integer(2)),
197///     Expression::mul(vec![Expression::integer(2), Expression::symbol(x.clone())]),
198///     Expression::integer(1),
199/// ]);
200///
201/// let result = try_extract_quadratic(&quadratic, &x);
202/// assert!(result.is_some());
203/// let (p, q) = result.unwrap();
204/// assert_eq!(p, Expression::integer(2));
205/// assert_eq!(q, Expression::integer(1));
206/// ```
207pub fn try_extract_quadratic(expr: &Expression, var: &Symbol) -> Option<(Expression, Expression)> {
208    if let Expression::Add(terms) = expr {
209        let mut x_squared_coeff = None;
210        let mut x_coeff = Expression::integer(0);
211        let mut constant = Expression::integer(0);
212
213        for term in terms.iter() {
214            match term {
215                Expression::Pow(base, exp) => {
216                    if let (Expression::Symbol(s), Expression::Number(Number::Integer(2))) =
217                        (base.as_ref(), exp.as_ref())
218                    {
219                        if *s == *var && x_squared_coeff.is_none() {
220                            x_squared_coeff = Some(Expression::integer(1));
221                            continue;
222                        }
223                    }
224                    return None;
225                }
226                Expression::Mul(factors) => {
227                    let mut has_x_squared = false;
228                    let mut has_x = false;
229                    let mut coeff = Expression::integer(1);
230
231                    for factor in factors.iter() {
232                        if let Expression::Pow(base, exp) = factor {
233                            if let (Expression::Symbol(s), Expression::Number(Number::Integer(2))) =
234                                (base.as_ref(), exp.as_ref())
235                            {
236                                if *s == *var {
237                                    has_x_squared = true;
238                                    continue;
239                                }
240                            }
241                        }
242                        if let Expression::Symbol(s) = factor {
243                            if *s == *var {
244                                has_x = true;
245                                continue;
246                            }
247                        }
248                        coeff = Expression::mul(vec![coeff, factor.clone()]);
249                    }
250
251                    if has_x_squared && x_squared_coeff.is_none() {
252                        x_squared_coeff = Some(coeff);
253                    } else if has_x {
254                        x_coeff = Expression::add(vec![x_coeff, coeff]);
255                    } else {
256                        constant = Expression::add(vec![constant, term.clone()]);
257                    }
258                }
259                Expression::Symbol(s) if *s == *var => {
260                    x_coeff = Expression::add(vec![x_coeff, Expression::integer(1)]);
261                }
262                _ => {
263                    constant = Expression::add(vec![constant, term.clone()]);
264                }
265            }
266        }
267
268        if x_squared_coeff == Some(Expression::integer(1)) {
269            return Some((x_coeff.simplify(), constant.simplify()));
270        }
271    }
272
273    None
274}