| 1857 | const out = terms.map((t) => t.div(xk)); |
| 1858 | return out.length === 1 ? out[0] : ce.function('Add', out); |
| 1859 | }; |
| 1860 | return divTerms(num).div(divTerms(den)); |
| 1861 | } |
| 1862 | |
| 1863 | /** R26B: rewrite a (possibly deeply nested) rational function of `x` into a |
| 1864 | * single FLAT `N/D` whose numerator and denominator are fully-expanded |
| 1865 | * polynomials in `x` sharing no common `x^k` monomial factor. Used by the |
| 1866 | * hyperbolic function-of-exponential fallback: the `t = e^x` substitution |
| 1867 | * lands `∫1/(a+b·sinh x)` at the nested shape `1/(x·(a + b/2·(x − 1/x)))`, |
| 1868 | * which no bundled rule matches, whereas the flat equivalent `2/(b·x²+2a·x−b)` |
| 1869 | * closes via the 1.2.1.1 quadratic-denominator rules. |
| 1870 | * |
| 1871 | * `asNumDen` cross-multiplies the nested Divide/Subtract/Power structure into a |
| 1872 | * single fraction; `expand` turns each side into a polynomial in x; the common |
| 1873 | * `x^k` factor (introduced by the outer `1/x` of the substitution) is cancelled |
| 1874 | * the way `Cancel` would. Returns null when the result is NOT a rational |
| 1875 | * function of x (some expanded side is not a polynomial in x, or the |
| 1876 | * denominator vanishes), so the caller can fall back to the un-normalized form. |
| 1877 | */ |
| 1878 | export function rationalNormalFormX( |
| 1879 | e: Expression, |
| 1880 | x: string, |
| 1881 | clearNegatives = false |
| 1882 | ): Expression | null { |
| 1883 | const ce = e.engine; |
| 1884 | const divByXk = (u: Expression, kk: number): Expression => { |
| 1885 | if (kk <= 0) return u; |
| 1886 | const xk = ce.function('Power', [ce.symbol(x), ce.number(kk)]); |
| 1887 | const terms = u.operator === 'Add' && u.ops ? u.ops : [u]; |
| 1888 | const out = terms.map((t) => t.div(xk)); |
| 1889 | return expand(out.length === 1 ? out[0] : ce.function('Add', out)); |
| 1890 | }; |
| 1891 | const { num, den } = asNumDen(e); |
| 1892 | let n = expand(num); |
| 1893 | let d = expand(den); |
| 1894 | if (d.isSame(0)) return null; |
| 1895 | // R30: a hyperbolic power ≥ 2 substitutes to `(½(x∓1/x))^k` folds whose |
| 1896 | // expansion carries genuine NEGATIVE x-powers (`x^-1`, `x^-2`, …); the outer |
| 1897 | // `1/x` of the substitution clears only ONE of them, so the residual is a |
| 1898 | // Laurent (not ordinary) polynomial and `polyCoeffsX` below rejects it, |
| 1899 | // leaving these rows unsolved. Multiply num & den through by `x^(-negShift)` |
| 1900 | // (the shared factor preserves the fraction's value) so both become genuine |
| 1901 | // polynomials in x; the biquadratic/trinomial denominator that results is |
| 1902 | // exactly what the bundled 1.2.x rules close. Gated (default off) so R26B and |
| 1903 | // ch2 stay byte-identical unless the caller opts in. |
| 1904 | if (clearNegatives) { |
| 1905 | const negShift = Math.min(0, minXPowerSigned(n, x), minXPowerSigned(d, x)); |
| 1906 | if (negShift < 0) { |
| 1907 | const xs = ce.function('Power', [ce.symbol(x), ce.number(-negShift)]); |
| 1908 | n = expand(n.mul(xs)); |
| 1909 | d = expand(d.mul(xs)); |
| 1910 | } |
| 1911 | } |
| 1912 | // Cancel the common `x^k` monomial factor shared by numerator and denominator |
| 1913 | // (introduced by the substitution's outer `1/x`). |
| 1914 | const k = Math.min(minXPower(n, x), minXPower(d, x)); |
| 1915 | n = divByXk(n, k); |
| 1916 | d = divByXk(d, k); |