Polynomial Factorization Tool

Results will be displayed here.

Enter coefficients and click "Factorize".

'; downloadBtn.style.display = 'none'; factorizationResult = null; } // --- Input Reading --- function getCoefficients() { const coeffs = []; let isValid = true; let allZero = true; for (let i = currentDegree; i >= 0; i--) { const input = document.getElementById(`pft-coeff-${i}`); const valueStr = input.value.trim(); input.style.borderColor = ''; // Reset border if (valueStr === '') { coeffs[currentDegree - i] = new Fraction(0); // Treat empty as zero continue; // Skip validation for empty, but don't mark as invalid immediately unless it's the leading one maybe } try { // Use Fraction.js to parse potential fractions or decimals const fracValue = new Fraction(valueStr); coeffs[currentDegree - i] = fracValue; if (fracValue.valueOf() !== 0) allZero = false; } catch (e) { // Handle parsing errors (e.g., non-numeric input) input.style.borderColor = 'var(--pft-error-color)'; isValid = false; coeffs[currentDegree - i] = NaN; // Mark as invalid } } // Check leading coefficient const leadingInput = document.getElementById(`pft-coeff-${currentDegree}`); if(coeffs[0].valueOf() === 0 && !allZero) { leadingInput.style.borderColor = 'var(--pft-error-color)'; isValid = false; resultsDiv.innerHTML = '

Error: Leading coefficient (for highest degree) cannot be zero unless the polynomial is entirely zero.

'; downloadBtn.style.display = 'none'; factorizationResult = null; return null; } if (!isValid) { resultsDiv.innerHTML = '

Error: Please enter valid numbers or fractions (like 3/4) for all coefficients.

'; downloadBtn.style.display = 'none'; factorizationResult = null; return null; } return coeffs; // Returns array of Fraction objects [a_n, a_{n-1}, ..., a_0] } // --- Polynomial Evaluation P(x) using Horner's method (with Fractions) --- function evaluatePolynomial(coeffs /* Fraction[] */, x /* Fraction */) { let result = new Fraction(0); for (let i = 0; i < coeffs.length; i++) { result = result.mul(x).add(coeffs[i]); } return result; } // --- Synthetic Division (with Fractions) --- // Divides polynomial 'coeffs' by (x - root) function syntheticDivision(coeffs /* Fraction[] */, root /* Fraction */) { const n = coeffs.length - 1; if (n < 1) return { quotient: [], remainder: coeffs[0] || new Fraction(0) }; // Cannot divide constant let quotient = [coeffs[0]]; for (let i = 1; i < n; i++) { quotient[i] = coeffs[i].add(quotient[i - 1].mul(root)); } let remainder = coeffs[n].add(quotient[n - 1].mul(root)); // Check if remainder is numerically zero if (Math.abs(remainder.valueOf()) < ZERO_TOLERANCE) { remainder = new Fraction(0); } return { quotient, remainder }; } // --- Find Integer Divisors --- function getDivisors(n /* integer */) { n = Math.abs(n); if (n === 0) return [0]; const divisors = new Set(); for (let i = 1; i * i <= n; i++) { if (n % i === 0) { divisors.add(i); divisors.add(n / i); } } return Array.from(divisors); } // --- Factorization Core Logic --- function factorPolynomial(coeffs /* Fraction[] */) { let currentCoeffs = [...coeffs]; const factors = []; let leadingCoefficient = new Fraction(1); // Keep track of overall scaling factor // 0. Handle Zero Polynomial if (currentCoeffs.every(c => c.valueOf() === 0)) { return { original: coeffs, factoredString: "0", factors: [{type: 'poly', coeffs: [new Fraction(0)]}], leadingCoefficient: new Fraction(0) }; } // 1. Factor out GCD of coefficients & Handle leading coefficient sign let numCoeffs = currentCoeffs.map(c => c.n * (c.d < 0 ? -1 : 1)); // Numerators (normalized denominator sign) let denCoeffs = currentCoeffs.map(c => Math.abs(c.d)); // Denominators (positive) let numGcd = arrayGcd(numCoeffs); let denLcm = denCoeffs.reduce((a,b) => (a*b) / gcd(a,b), 1); // LCM of denominators let commonFactor = new Fraction(numGcd, denLcm); // Ensure leading coefficient is positive after factoring out commonFactor if (currentCoeffs[0].valueOf() < 0 && commonFactor.valueOf() > 0) { commonFactor = commonFactor.mul(-1); } else if (currentCoeffs[0].valueOf() > 0 && commonFactor.valueOf() < 0) { commonFactor = commonFactor.mul(-1); // Should not happen if GCD/LCM logic is right, but safety check } if (commonFactor.abs().valueOf() !== 1) { leadingCoefficient = leadingCoefficient.mul(commonFactor); currentCoeffs = currentCoeffs.map(c => c.div(commonFactor)); } // Now currentCoeffs should have integer coefficients if possible, or simpler fractions // Convert to integers if all denominators are 1 if(currentCoeffs.every(c => c.d === 1)) { currentCoeffs = currentCoeffs.map(c => c.n); // Use integer array for divisor finding } else { // Keep as fractions, Rational Root Theorem still applies } // Ensure internal representation has integer coefficients for RRT if possible let internalCoeffs = currentCoeffs.map(c => new Fraction(c)); // Work with fractions internally // --- Rational Root Theorem --- while (internalCoeffs.length > 3) { // While degree > 2 const degree = internalCoeffs.length - 1; const a_0 = internalCoeffs[degree]; const a_n = internalCoeffs[0]; // Need integer coefficients for standard RRT divisor finding // Find LCM of denominators and multiply through let lcm_denominators = internalCoeffs.reduce((lcm, c) => (lcm * c.d) / gcd(lcm, c.d), 1); let integerCoeffs = internalCoeffs.map(c => c.mul(lcm_denominators).n); // Get integer coefficients const constantTerm = integerCoeffs[degree]; const leadingTerm = integerCoeffs[0]; if (Math.abs(constantTerm) < ZERO_TOLERANCE) { // If constant term is 0, factor out x factors.push({ type: 'linear', root: new Fraction(0) }); internalCoeffs.pop(); // Remove the zero constant term (equivalent to dividing by x) // Adjust overall leading coefficient if necessary (multiplying by 1/lcm_denominators?) - handled later leadingCoefficient = leadingCoefficient.div(lcm_denominators); // Account for the scaling we did continue; // Restart loop with reduced polynomial } const p_divisors = getDivisors(constantTerm); const q_divisors = getDivisors(leadingTerm); let foundRoot = false; potentialRootsLoop: for (const p of p_divisors) { for (const q of q_divisors) { if (q === 0) continue; const potentialRoot = new Fraction(p, q); // Test positive root let remainder = evaluatePolynomial(internalCoeffs, potentialRoot); if (Math.abs(remainder.valueOf()) < ZERO_TOLERANCE) { factors.push({ type: 'linear', root: potentialRoot }); const { quotient } = syntheticDivision(internalCoeffs, potentialRoot); internalCoeffs = quotient; foundRoot = true; break potentialRootsLoop; } // Test negative root const potentialNegRoot = potentialRoot.mul(-1); remainder = evaluatePolynomial(internalCoeffs, potentialNegRoot); if (Math.abs(remainder.valueOf()) < ZERO_TOLERANCE) { factors.push({ type: 'linear', root: potentialNegRoot }); const { quotient } = syntheticDivision(internalCoeffs, potentialNegRoot); internalCoeffs = quotient; foundRoot = true; break potentialRootsLoop; } } } // End potential roots loop if (!foundRoot) { // No more rational roots for this degree, break the RRT loop break; } // Rescale internalCoeffs back if we scaled by LCM? Or just keep going? // The synthetic division result should maintain the correct relative coefficients. } // End while degree > 2 // --- Handle Remaining Polynomial (Linear, Quadratic, or higher if no rational roots found) --- if (internalCoeffs.length > 1 && !(internalCoeffs.length === 1 && internalCoeffs[0].valueOf() === 1)) { // If remaining is not just '1' // If quadratic, check if it factors over rationals if (internalCoeffs.length === 3) { const [a, b, c] = internalCoeffs; const discriminant = b.mul(b).sub(a.mul(c).mul(4)); // b^2 - 4ac as Fraction if (discriminant.valueOf() >= -ZERO_TOLERANCE) { // Check if discriminant is non-negative const sqrtDiscriminant = Math.sqrt(discriminant.valueOf()); if (Math.abs(sqrtDiscriminant - Math.round(sqrtDiscriminant)) < ZERO_TOLERANCE) { // Check if sqrt is integer/rational // Check if sqrt(discriminant) can be represented as a fraction m/n such that m^2/n^2 = discriminant let sqrtDiscFrac = null; try { sqrtDiscFrac = new Fraction(Math.sqrt(discriminant.n), Math.sqrt(discriminant.d)); // Verify: sqrtDiscFrac * sqrtDiscFrac === discriminant? if (sqrtDiscFrac.mul(sqrtDiscFrac).sub(discriminant).abs().valueOf() < ZERO_TOLERANCE) { // Discriminant is a perfect square of a rational number const twoA = a.mul(2); const root1 = b.neg().add(sqrtDiscFrac).div(twoA); const root2 = b.neg().sub(sqrtDiscFrac).div(twoA); factors.push({ type: 'linear', root: root1 }); factors.push({ type: 'linear', root: root2 }); // The quadratic factor is now fully reduced internalCoeffs = [new Fraction(1)]; // Represents the factored out parts } else { // Square root isn't rational, keep quadratic factor factors.push({ type: 'poly', coeffs: internalCoeffs }); internalCoeffs = [new Fraction(1)]; } } catch (e) { // Cannot take sqrt easily, likely irrational. Keep quadratic factor. factors.push({ type: 'poly', coeffs: internalCoeffs }); internalCoeffs = [new Fraction(1)]; } } else { // Discriminant positive but not a perfect square -> irrational roots factors.push({ type: 'poly', coeffs: internalCoeffs }); internalCoeffs = [new Fraction(1)]; } } else { // Discriminant negative -> complex roots factors.push({ type: 'poly', coeffs: internalCoeffs }); internalCoeffs = [new Fraction(1)]; } } else { // Remaining polynomial is linear or higher degree but irreducible over rationals factors.push({ type: 'poly', coeffs: internalCoeffs }); internalCoeffs = [new Fraction(1)]; } } // Final assembly of factored string let factoredString = ""; if (leadingCoefficient.abs().valueOf() !== 1 || leadingCoefficient.s < 0) { factoredString += formatFrac(leadingCoefficient) + " "; } factoredString += factors.map(formatFactor).join(" "); // Clean up extra spaces factoredString = factoredString.replace(/\s+/g, ' ').trim(); if (factoredString === "") factoredString = "1"; // Handle case where input was a constant return { original: coeffs, factoredString: factoredString, factors: factors, leadingCoefficient: leadingCoefficient }; } // --- Display Results --- function displayResults(result) { resultsDiv.innerHTML = ''; // Clear previous // Use original coefficients (passed as Fractions) for display const originalPolyString = formatPolynomial(result.original); resultsDiv.innerHTML += `

Original Polynomial:
${originalPolyString}

`; resultsDiv.innerHTML += `

Factored Form (over Rationals):
${result.factoredString}

`; // Add a note about irreducibility if applicable const hasIrreducible = result.factors.some(f => f.type === 'poly' && f.coeffs.length > 2); // Degree > 1 if (hasIrreducible) { resultsDiv.innerHTML += `

Note: Factors shown are irreducible over the rational numbers. Quadratic or higher degree factors may be factorable over real or complex numbers.

`; } factorizationResult = result; // Store for PDF downloadBtn.style.display = 'inline-block'; // Show download button } // --- Generate and Download PDF --- function downloadPDF() { if (!factorizationResult) return; const doc = new jsPDF(); const { original, factoredString } = factorizationResult; // --- Get Colors --- const styles = getComputedStyle(document.querySelector('.pft-tool-container')); const primaryColor = styles.getPropertyValue('--pft-primary-color').trim(); const textColor = styles.getPropertyValue('--pft-text-color').trim(); // --- PDF Content --- const lineHeight = 8; // Increased line height for readability const margin = 15; let currentY = 20; // Title doc.setFontSize(18); doc.setTextColor(primaryColor); doc.text("Polynomial Factorization Results", doc.internal.pageSize.getWidth() / 2, currentY, { align: 'center' }); currentY += lineHeight * 2; // Original Polynomial doc.setFontSize(12); doc.setTextColor(textColor); doc.setFont("helvetica", "bold"); doc.text("Original Polynomial:", margin, currentY); currentY += lineHeight; doc.setFont("courier", "normal"); // Wrap long polynomial strings const originalPolyLines = doc.splitTextToSize(formatPolynomial(original), doc.internal.pageSize.getWidth() - 2 * margin - 10); doc.text(originalPolyLines, margin + 5, currentY); currentY += originalPolyLines.length * (lineHeight * 0.8); // Adjust Y based on wrapped lines currentY += lineHeight; // Extra space // Factored Form doc.setFont("helvetica", "bold"); doc.setTextColor(textColor); doc.text("Factored Form (over Rationals):", margin, currentY); currentY += lineHeight; doc.setFont("courier", "normal"); // Wrap long factored strings const factoredLines = doc.splitTextToSize(factoredString, doc.internal.pageSize.getWidth() - 2 * margin - 10); doc.text(factoredLines, margin + 5, currentY); currentY += factoredLines.length * (lineHeight * 0.8); // Add note if needed const hasIrreducible = factorizationResult.factors.some(f => f.type === 'poly' && f.coeffs.length > 2); if (hasIrreducible) { currentY += lineHeight * 1.5; doc.setFont("helvetica", "italic"); doc.setFontSize(10); doc.setTextColor(100); // Grey color for note const note = "Note: Factors shown are irreducible over the rational numbers. Quadratic or higher degree factors may be factorable over real or complex numbers."; const noteLines = doc.splitTextToSize(note, doc.internal.pageSize.getWidth() - 2 * margin - 10); doc.text(noteLines, margin + 5, currentY); } // --- Save PDF --- doc.save('polynomial_factorization.pdf'); } // --- Event Listeners --- degreeSelector.addEventListener('change', (e) => { generateInputs(e.target.value); }); factorizeBtn.addEventListener('click', () => { const coeffs = getCoefficients(); // Returns Fractions if (coeffs) { try { const result = factorPolynomial(coeffs); displayResults(result); } catch (error) { console.error("Factorization Error:", error); resultsDiv.innerHTML = `

An unexpected error occurred during factorization. Please check your input. ${error.message || ''}

`; downloadBtn.style.display = 'none'; factorizationResult = null; } } }); downloadBtn.addEventListener('click', downloadPDF); // Add input listeners to clear errors (basic) coefficientsContainer.addEventListener('input', (e) => { if (e.target.tagName === 'INPUT') { e.target.style.borderColor = ''; if (resultsDiv.querySelector('.pft-error')) { resultsDiv.innerHTML = '

Enter coefficients and click "Factorize".

'; downloadBtn.style.display = 'none'; factorizationResult = null; } } }); // --- Initial Setup --- generateInputs(degreeSelector.value); // Initialize with default degree })(); // IIFE
Scroll to Top