( body: Expression, indexes: ReadonlyArray<Expression>, fn: (acc: T, x: Expression) => T | null, initial: T )
| 559 | */ |
| 560 | export function symbolicProductClosedForm( |
| 561 | body: Expression | undefined, |
| 562 | limits: Expression, |
| 563 | ce: ComputeEngine |
| 564 | ): Expression | undefined { |
| 565 | if (!body || !isFunction(limits, 'Limits')) return undefined; |
| 566 | const index = isSymbol(limits.op1) ? limits.op1.symbol : undefined; |
| 567 | const lower = limits.op2; |
| 568 | const upper = limits.op3; |
| 569 | if (!index || !lower || !upper) return undefined; |
| 570 | |
| 571 | // Π_{k=1}^{n} k = n! (bare index, lower bound 1). |
| 572 | if (isSymbol(body) && body.symbol === index && lower.isSame(1)) |
| 573 | return ce.function('Factorial', [upper]); |
| 574 | |
| 575 | // Telescoping product: body = h(k+1)/h(k). |
| 576 | const { num, den } = asSingleFraction(body, ce); |
| 577 | if (new Set(num.unknowns).has(index) && new Set(den.unknowns).has(index)) { |
| 578 | // forward: den shifted by k→k+1 equals num ⇒ body = h(k+1)/h(k), h = den. |
| 579 | if (shiftIndex(den, index, ce).isSame(num)) |
| 580 | return ce.function('Divide', [ |
| 581 | num.subs({ [index]: upper }), |
| 582 | den.subs({ [index]: lower }), |
| 583 | ]); |
| 584 | // mirror: num shifted by k→k+1 equals den ⇒ body = h(k)/h(k+1), h = num. |
| 585 | if (shiftIndex(num, index, ce).isSame(den)) |
| 586 | return ce.function('Divide', [ |
| 587 | num.subs({ [index]: lower }), |
| 588 | den.subs({ [index]: upper }), |
| 589 | ]); |
| 590 | } |
| 591 | |
| 592 | return undefined; |
| 593 | } |
| 594 | |
| 595 | /** |
| 596 | * Reformat an evaluated closed form so a rational multiple of a symbolic factor |
| 597 | * reads as a fraction: `Multiply(Rational(p, q), R)` → `Divide(p·R, q)` (and |
| 598 | * `Divide(R, q)` when `p = 1`). Mirrors the readability intent of the |
| 599 | * telescoping `Subtract` above (`π²/6` instead of `(1/6)·π²`). Any other shape |
| 600 | * is returned unchanged. |
| 601 | */ |
| 602 | function asReadableFraction(z: Expression, ce: ComputeEngine): Expression { |
| 603 | if (!isFunction(z, 'Multiply')) return z; |
| 604 | let coeff: Expression | undefined; |
| 605 | const rest: Expression[] = []; |
| 606 | for (const op of z.ops) { |
| 607 | if (coeff === undefined && isNumber(op) && op.im === 0) coeff = op; |
| 608 | else rest.push(op); |
| 609 | } |
| 610 | if (coeff === undefined || rest.length === 0) return z; |
| 611 | const [num, den] = coeff.numeratorDenominator; |
| 612 | if (den.isSame(1)) return z; |
| 613 | const restExpr = |
| 614 | rest.length === 1 |
| 615 | ? rest[0] |
| 616 | : ce.function('Multiply', rest, { form: 'structural' }); |
| 617 | const numExpr = num.isSame(1) |
| 618 | ? restExpr |
no test coverage detected