diff --git a/src/main/java/com/thealgorithms/maths/EulerPseudoprime.java b/src/main/java/com/thealgorithms/maths/EulerPseudoprime.java new file mode 100644 index 000000000000..9a03f4e21d17 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/EulerPseudoprime.java @@ -0,0 +1,108 @@ +package com.thealgorithms.maths; + +import java.math.BigInteger; +import java.util.Random; + +/** + * The {@code EulerPseudoprime} class implements the Euler primality test. + * + * It is based on Euler’s criterion: + * For an odd prime number {@code n} and any integer {@code a} coprime to {@code n}: + * a^((n-1)/2) ≡ (a/n) (mod n) + * where (a/n) is the Jacobi symbol. + * + * This algorithm is a stronger probabilistic test than Fermat’s test. + * It may still incorrectly identify a composite as “probably prime” (Euler pseudoprime), + * but such cases are rare. + */ +public final class EulerPseudoprime { + + private EulerPseudoprime() { + // Private constructor to prevent instantiation. + } + + private static final Random RANDOM = new Random(1); + + /** + * Performs the Euler primality test for a given number. + * + * @param n number to test (must be > 2 and odd) + * @param trials number of random bases to test + * @return {@code true} if {@code n} passes all Euler tests (probably prime), + * {@code false} if composite. + */ + public static boolean isProbablePrime(BigInteger n, int trials) { + if (n.compareTo(BigInteger.TWO) < 0) { + return false; + } + if (n.equals(BigInteger.TWO) || n.equals(BigInteger.valueOf(3))) { + return true; + } + if (n.mod(BigInteger.TWO).equals(BigInteger.ZERO)) { + return false; + } + + for (int i = 0; i < trials; i++) { + BigInteger a = uniformRandom(BigInteger.TWO, n.subtract(BigInteger.TWO)); + BigInteger jacobi = BigInteger.valueOf(jacobiSymbol(a, n)); + if (jacobi.equals(BigInteger.ZERO)) { + return false; + } + + BigInteger exp = n.subtract(BigInteger.ONE).divide(BigInteger.TWO); + BigInteger modExp = a.modPow(exp, n); + + // Euler's criterion: a^((n-1)/2) ≡ (a/n) (mod n) + if (!modExp.equals(jacobi.mod(n))) { + return false; // definitely composite + } + } + return true; // probably prime + } + + /** + * Computes the Jacobi symbol (a/n). + * Assumes n is positive and odd. + */ + public static int jacobiSymbol(BigInteger a, BigInteger n) { + if (n.signum() <= 0 || n.mod(BigInteger.TWO).equals(BigInteger.ZERO)) { + throw new IllegalArgumentException("n must be positive and odd."); + } + + int result = 1; + a = a.mod(n); + + while (a.compareTo(BigInteger.ZERO) != 0) { + while (a.mod(BigInteger.TWO).equals(BigInteger.ZERO)) { + a = a.divide(BigInteger.TWO); + BigInteger nMod8 = n.mod(BigInteger.valueOf(8)); + if (nMod8.equals(BigInteger.valueOf(3)) || nMod8.equals(BigInteger.valueOf(5))) { + result = -result; + } + } + + BigInteger temp = a; + a = n; + n = temp; + + if (a.mod(BigInteger.valueOf(4)).equals(BigInteger.valueOf(3)) && n.mod(BigInteger.valueOf(4)).equals(BigInteger.valueOf(3))) { + result = -result; + } + + a = a.mod(n); + } + + return n.equals(BigInteger.ONE) ? result : 0; + } + + /** + * Generates a random BigInteger between {@code min} and {@code max}, inclusive. + */ + private static BigInteger uniformRandom(BigInteger min, BigInteger max) { + BigInteger result; + do { + result = new BigInteger(max.bitLength(), RANDOM); + } while (result.compareTo(min) < 0 || result.compareTo(max) > 0); + return result; + } +} diff --git a/src/test/java/com/thealgorithms/maths/EulerPseudoprimeTest.java b/src/test/java/com/thealgorithms/maths/EulerPseudoprimeTest.java new file mode 100644 index 000000000000..b4ee0cf0659c --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/EulerPseudoprimeTest.java @@ -0,0 +1,93 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; + +import java.math.BigInteger; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class EulerPseudoprimeTest { + + @Test + void testPrimeNumbers() { + assertTrue(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(7), 5)); + assertTrue(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(13), 5)); + assertTrue(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(101), 5)); + } + + @Test + void testCompositeNumbers() { + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(9), 5)); + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(21), 5)); + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(221), 5)); + } + + @Test + void testEvenNumbers() { + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(4), 5)); + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(100), 5)); + } + + @Test + void testEdgeCases() { + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(0), 5)); + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(1), 5)); + assertTrue(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(2), 5)); + assertTrue(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(3), 5)); + } + + @Test + void testIsProbablePrimeWhenJacobiSymbolIsZero() { + try (MockedStatic mockedPrimality = Mockito.mockStatic(EulerPseudoprime.class, Mockito.CALLS_REAL_METHODS)) { + + // Mock jacobiSymbol to return 0 to test the branch + mockedPrimality.when(() -> EulerPseudoprime.jacobiSymbol(any(BigInteger.class), any(BigInteger.class))).thenReturn(0); + + boolean result = EulerPseudoprime.isProbablePrime(BigInteger.valueOf(15), 1); + + assertFalse(result); + } + } + + @Test + void testJacobiSymbolThrowsForEvenOrNonPositiveN() throws Exception { + var method = EulerPseudoprime.class.getDeclaredMethod("jacobiSymbol", BigInteger.class, BigInteger.class); + + // Helper lambda to unwrap InvocationTargetException + Runnable invokeJacobi = () -> { + try { + method.invoke(null, BigInteger.valueOf(2), BigInteger.valueOf(8)); + } catch (Exception e) { + // unwrap + Throwable cause = e.getCause(); + if (cause instanceof IllegalArgumentException) { + throw (IllegalArgumentException) cause; + } else { + throw new RuntimeException(e); + } + } + }; + + // Now check that it actually throws + assertThrows(IllegalArgumentException.class, invokeJacobi::run); + + // Another case: non-positive n + Runnable invokeJacobi2 = () -> { + try { + method.invoke(null, BigInteger.valueOf(5), BigInteger.valueOf(-3)); + } catch (Exception e) { + Throwable cause = e.getCause(); + if (cause instanceof IllegalArgumentException) { + throw (IllegalArgumentException) cause; + } else { + throw new RuntimeException(e); + } + } + }; + assertThrows(IllegalArgumentException.class, invokeJacobi2::run); + } +}