( expr: Expression, variable: string )
| 296 | } |
| 297 | |
| 298 | /** |
| 299 | * Detect if an expression is a difference of squares. |
| 300 | * Returns the factored form (a-b)(a+b) if successful, null otherwise. |
| 301 | * |
| 302 | * Pattern: a² - b² → (a-b)(a+b) |
| 303 | * |
| 304 | * IMPORTANT: Does not call .simplify() on the result to avoid infinite recursion. |
| 305 | */ |
| 306 | export function factorDifferenceOfSquares(expr: Expression): Expression | null { |
| 307 | const ce = expr.engine; |
| 308 | |
| 309 | // Must be an Add expression with exactly 2 terms (one positive, one negative) |
| 310 | if (!isFunction(expr, 'Add')) return null; |
| 311 | |
| 312 | const terms = expr.ops; |
| 313 | if (terms.length !== 2) return null; |
| 314 | |
| 315 | // Try to extract square roots of both terms |
| 316 | // One should be positive, one negative |
| 317 | const results: Array<{ |
| 318 | sqrt: Expression; |
| 319 | isNegative: boolean; |
| 320 | }> = []; |
| 321 | |
| 322 | for (const term of terms) { |
| 323 | // Check if term is negative |
| 324 | let isNeg = isFunction(term, 'Negate'); |
| 325 | let absTerm = isNeg && isFunction(term) ? term.op1 : term; |
| 326 | |
| 327 | // Also handle negative numeric literals |
| 328 | if (!isNeg && isNumber(term) && term.isNegative === true) { |
| 329 | isNeg = true; |
| 330 | absTerm = term.neg(); // Get the absolute value |
| 331 | } |
| 332 | |
| 333 | // Also handle negative terms from Multiply with negative coefficient |
| 334 | if (!isNeg && isFunction(term, 'Multiply')) { |
| 335 | const ops = term.ops; |
| 336 | // Check if first operand is negative number |
| 337 | if (isNumber(ops[0]) && ops[0].isNegative === true) { |
| 338 | isNeg = true; |
| 339 | // Create positive version by negating the coefficient |
| 340 | const newOps = [ops[0].neg(), ...ops.slice(1)]; |
| 341 | absTerm = ce.expr(['Multiply', ...newOps.map((op) => op.json)]); |
| 342 | } |
| 343 | } |
| 344 | |
| 345 | const sqrt = extractSquareRoot(absTerm); |
| 346 | if (sqrt === null) return null; |
| 347 | |
| 348 | results.push({ sqrt, isNegative: isNeg }); |
| 349 | } |
| 350 | |
| 351 | // We need exactly one positive and one negative square |
| 352 | const posSquares = results.filter((r) => !r.isNegative); |
| 353 | const negSquares = results.filter((r) => r.isNegative); |
| 354 | |
| 355 | if (posSquares.length !== 1 || negSquares.length !== 1) return null; |
no test coverage detected