Repository: keon/algorithms

Issue:
Fix TODO in `polynomial.py`
When working on #761 we discovered an unfixed TODO here: https://github.com/keon/algorithms/blob/15e5292d21f1fde70a4cdb428c25db0192b5b7f0/algorithms/maths/polynomial.py#L444

PR is coming soon.


Discussion:
I'd like to work on this!



Patch:
diff --git a/algorithms/maths/polynomial.py b/algorithms/maths/polynomial.py
index 07a0c3b8a..81faf2b7e 100644
--- a/algorithms/maths/polynomial.py
+++ b/algorithms/maths/polynomial.py
@@ -438,10 +438,7 @@ def __floordiv__(self, other: Union[int, float, Fraction, Monomial]):
     # def __truediv__(self, other: Union[int, float, Fraction, Monomial, Polynomial]) -> Polynomial:
     def __truediv__(self, other: Union[int, float, Fraction, Monomial]):
         """
-        For Polynomials, only division by a monomial
-        is defined.
-
-        TODO: Implement polynomial / polynomial.
+        For Polynomial division, no remainder is provided. Must use poly_long_division() to capture remainder
         """
         if isinstance(other, int) or isinstance(other, float) or isinstance(other, Fraction):
             return self.__truediv__( Monomial({}, other) )
@@ -449,17 +446,11 @@ def __truediv__(self, other: Union[int, float, Fraction, Monomial]):
             poly_temp = reduce(lambda acc, val: acc + val, map(lambda x: x / other, [z for z in self.all_monomials()]), Polynomial([Monomial({}, 0)]))
             return poly_temp
         elif isinstance(other, Polynomial):
-            if Monomial({}, 0) in other.all_monomials():
-                if len(other.all_monomials()) == 2:
-                    temp_set = {x for x in other.all_monomials() if x != Monomial({}, 0)}
-                    only = temp_set.pop()
-                    return self.__truediv__(only)
-            elif len(other.all_monomials()) == 1:
-                temp_set = {x for x in other.all_monomials()}
-                only = temp_set.pop()
-                return self.__truediv__(only)
-
-        raise ValueError('Can only divide a polynomial by an int, float, Fraction, or a Monomial.')
+            # Call long division
+            quotient, remainder = self.poly_long_division(other)
+            return quotient  # Return just the quotient, remainder is ignored here
+
+        raise ValueError('Can only divide a polynomial by an int, float, Fraction, Monomial, or Polynomial.')
 
         return
 
@@ -526,7 +517,59 @@ def subs(self, substitutions: Union[int, float, Fraction, Dict[int, Union[int, f
 
     def __str__(self) -> str:
         """
-        Get a string representation of
-        the polynomial.
+        Get a properly formatted string representation of the polynomial.
+        """
+        sorted_monos = sorted(self.all_monomials(), key=lambda m: sorted(m.variables.items(), reverse=True),
+                              reverse=True)
+        return ' + '.join(str(m) for m in sorted_monos if m.coeff != Fraction(0, 1))
+
+    def poly_long_division(self, other: 'Polynomial') -> tuple['Polynomial', 'Polynomial']:
         """
-        return ' + '.join(str(m) for m in self.all_monomials() if m.coeff != Fraction(0, 1))
+        Perform polynomial long division
+        Returns (quotient, remainder)
+        """
+        if not isinstance(other, Polynomial):
+            raise ValueError("Can only divide by another Polynomial.")
+
+        if len(other.all_monomials()) == 0:
+            raise ValueError("Cannot divide by zero polynomial.")
+
+        quotient = Polynomial([])
+        remainder = self.clone()
+
+        divisor_monos = sorted(other.all_monomials(), key=lambda m: sorted(m.variables.items(), reverse=True),
+                               reverse=True)
+        divisor_lead = divisor_monos[0]
+
+        while remainder.all_monomials() and max(remainder.variables(), default=-1) >= max(other.variables(),
+                                                                                          default=-1):
+            remainder_monos = sorted(remainder.all_monomials(), key=lambda m: sorted(m.variables.items(), reverse=True),
+                                     reverse=True)
+            remainder_lead = remainder_monos[0]
+
+            if not all(remainder_lead.variables.get(var, 0) >= divisor_lead.variables.get(var, 0) for var in
+                       divisor_lead.variables):
+                break
+
+            lead_quotient = remainder_lead / divisor_lead
+            quotient = quotient + Polynomial([lead_quotient])  # Convert Monomial to Polynomial
+
+            remainder = remainder - (
+                        Polynomial([lead_quotient]) * other)  # Convert Monomial to Polynomial before multiplication
+
+        return quotient, remainder
+
+dividend = Polynomial([
+    Monomial({1: 3}, 4),   # 4(a_1)^3
+    Monomial({1: 2}, 3),   # 3(a_1)^2
+    Monomial({1: 1}, -2),  # -2(a_1)
+    Monomial({}, 5)        # +5
+])
+
+divisor = Polynomial([
+    Monomial({1: 1}, 2),   # 2(a_1)
+    Monomial({}, -1)       # -1
+])
+
+quotient = dividend / divisor
+print("Quotient:", quotient)


Test patch:
diff --git a/tests/test_polynomial.py b/tests/test_polynomial.py
index 4ba2ba6b7..ab5674017 100644
--- a/tests/test_polynomial.py
+++ b/tests/test_polynomial.py
@@ -105,27 +105,6 @@ def test_polynomial_multiplication(self):
 		]))
 		return
 
-	def test_polynomial_division(self):
-
-		# Should raise a ValueError if the divisor is not a monomial
-		# or a polynomial with only one term.
-		self.assertRaises(ValueError, lambda x, y: x / y, self.p5, self.p3)
-		self.assertRaises(ValueError, lambda x, y: x / y, self.p6, self.p4)
-
-		self.assertEqual(self.p3 / self.p2, Polynomial([
-			Monomial({}, 1),
-			Monomial({1: 1, 2: -1}, 0.75)
-		]))
-		self.assertEqual(self.p7 / self.m1, Polynomial([
-			Monomial({1: -1, 2: -3}, 2),
-			Monomial({1: 0, 2: -4}, 1.5)
-		]))
-		self.assertEqual(self.p7 / self.m1, Polynomial([
-			Monomial({1: -1, 2: -3}, 2),
-			Monomial({2: -4}, 1.5)
-		]))
-		return
-
 	def test_polynomial_variables(self):
 		# The zero polynomial has no variables.
 
@@ -172,4 +151,51 @@ def test_polynomial_clone(self):
 		self.assertEqual(self.p5.clone(), Polynomial([
 			Monomial({1: -1, 3: 2}, 1)
 		]))
-		return
\ No newline at end of file
+		return
+
+	def test_polynomial_long_division(self):
+		"""
+        Test polynomial long division
+        """
+
+		# Dividend: 4a_1^3 + 3a_1^2 - 2a_1 + 5
+		dividend = Polynomial([
+			Monomial({1: 3}, 4),  # 4(a_1)^3
+			Monomial({1: 2}, 3),  # 3(a_1)^2
+			Monomial({1: 1}, -2),  # -2(a_1)
+			Monomial({}, 5)  # +5
+		])
+
+		# Divisor: 2a_1 - 1
+		divisor = Polynomial([
+			Monomial({1: 1}, 2),  # 2(a_1)
+			Monomial({}, -1)  # -1
+		])
+
+		# Expected Quotient: 2a_1^2 + (5/2)a_1 + 1/4
+		expected_quotient = Polynomial([
+			Monomial({1: 2}, 2),  # 2(a_1)^2
+			Monomial({1: 1}, Fraction(5, 2)),  # (5/2)(a_1)
+			Monomial({}, Fraction(1, 4))  # +1/4
+		])
+
+		# Expected Remainder: 21/4
+		expected_remainder = Polynomial([
+			Monomial({}, Fraction(21, 4))  # 21/4
+		])
+
+		quotient_long_div, remainder_long_div = dividend.poly_long_division(divisor)
+
+		quotient_truediv = dividend / divisor  # Calls __truediv__, which returns only the quotient
+
+		# Check if quotient from poly_long_division matches expected
+		self.assertEqual(quotient_long_div, expected_quotient)
+
+		# Check if remainder from poly_long_division matches expected
+		self.assertEqual(remainder_long_div, expected_remainder)
+
+		# Check if quotient from __truediv__ matches quotient from poly_long_division
+		self.assertEqual(quotient_truediv, quotient_long_div)
+
+		return
+