diff --git a/README.md b/README.md index ae7f21c..aadeffe 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Requires Java 25. Maven comes with the wrapper. src/main/java/io/github/diegourban/algorithms/ datastructure/ array (shuffle), binarytree, linkedlist, queue, set, stack, vector io/ Reader: fast tokenising stdin reader for judge-style input - math/ Algebra (gcd, lcm, primes, factorisation), Combinatorics, Geometry, Numbers, Statistics + math/ NumberTheory (gcd, lcm, primes, factorisation), Sequences (Fibonacci), Combinatorics, Geometry, Measurement, Numbers, Statistics search/ BinarySearch.indexOf sorting/ Sorter interface: Insertion, Merge, Quick and Selection sort string/ StringUtils diff --git a/src/main/java/io/github/diegourban/algorithms/math/Algebra.java b/src/main/java/io/github/diegourban/algorithms/math/NumberTheory.java similarity index 56% rename from src/main/java/io/github/diegourban/algorithms/math/Algebra.java rename to src/main/java/io/github/diegourban/algorithms/math/NumberTheory.java index 71f4458..b06647b 100644 --- a/src/main/java/io/github/diegourban/algorithms/math/Algebra.java +++ b/src/main/java/io/github/diegourban/algorithms/math/NumberTheory.java @@ -1,11 +1,11 @@ package io.github.diegourban.algorithms.math; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.math.RoundingMode; import java.util.*; -public class Algebra { +/** + * Number theory: greatest common divisor, least common multiple, primality and prime factorisation. + */ +public class NumberTheory { /** * Least common multiple (lcm) of two numbers. @@ -137,85 +137,4 @@ private static void fillPrimeFactors(int number, Collection factors) { } } - /** - * Calculates the nth Fibonacci number. - * Works with n <= 46 - *

- * Solution: Using a bottom-up approach, starting with the 0th fibonacci number and iteratively computing subsequent numbers until we get to n. - *

- * Complexity: N time and O(1) space. - * - * @param n the nth number in the Fibonacci sequence to calculate - * @return the nth Fibonacci number - */ - public static int fibonacci(int n) { - // edge cases: - if (n < 0) { - throw new IllegalArgumentException("Index was negative. No such thing as a negative index in a series."); - } else if (n == 0 || n == 1) { - return n; - } - - // we'll be building the fibonacci series from the bottom up - // so we'll need to track the previous 2 numbers at each step - int prevPrev = 0; // 0th fibonacci - int prev = 1; // 1st fibonacci - int current = 0; // Declare and initialize current - - for (int i = 1; i < n; i++) { - - // Iteration 1: current = 2nd fibonacci - // Iteration 2: current = 3rd fibonacci - // Iteration 3: current = 4th fibonacci - // To get nth fibonacci ... do n-1 iterations. - current = prev + prevPrev; - prevPrev = prev; - prev = current; - } - - return current; - } - - /** - * Calculates the nth Fibonacci number. - * Works with n <= 67 - * - * @param n the nth number in the Fibonacci sequence to calculate - * @return the nth Fibonacci number - */ - public static double fibonacciSimpleBinetFormula(int n) { - if (n == 0) { - return 0; - } - final double phi = (1 + Math.sqrt(5)) / 2; - final double sqrt5 = Math.sqrt(5); - return ((Math.pow(phi, n) - Math.pow(-phi, -n)) / sqrt5); - } - - /** - * Calculates the nth Fibonacci number. - * Works with n <= 71 - * - * @param n the nth number in the Fibonacci sequence to calculate - * @return the nth Fibonacci number - */ - public static BigInteger fibonacciBinetFormula(int n) { - if (n == 0) { - return new BigInteger("0"); - } - if (n == 1 || n == 2) { - return new BigInteger("1"); - } - if (n == 3) { - return new BigInteger("2"); - } - - final double phi = (1 + Math.sqrt(5)) / 2; - BigDecimal sqrt5 = new BigDecimal(Math.sqrt(5)); - - BigDecimal pow = new BigDecimal(Math.pow(phi, n)); - BigDecimal powInvrt = new BigDecimal(Math.pow(-phi, -n)); - return pow.subtract(powInvrt).divide(sqrt5, RoundingMode.HALF_UP).toBigInteger(); - } - } diff --git a/src/main/java/io/github/diegourban/algorithms/math/Sequences.java b/src/main/java/io/github/diegourban/algorithms/math/Sequences.java new file mode 100644 index 0000000..031578f --- /dev/null +++ b/src/main/java/io/github/diegourban/algorithms/math/Sequences.java @@ -0,0 +1,96 @@ +package io.github.diegourban.algorithms.math; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; + +/** + * Number sequences. + */ +public class Sequences { + + private Sequences() { + } + + /** + * Calculates the nth Fibonacci number. + * Works with n <= 46 + *

+ * Solution: Using a bottom-up approach, starting with the 0th fibonacci number and iteratively computing subsequent numbers until we get to n. + *

+ * Complexity: N time and O(1) space. + * + * @param n the nth number in the Fibonacci sequence to calculate + * @return the nth Fibonacci number + */ + public static int fibonacci(int n) { + // edge cases: + if (n < 0) { + throw new IllegalArgumentException("Index was negative. No such thing as a negative index in a series."); + } else if (n == 0 || n == 1) { + return n; + } + + // we'll be building the fibonacci series from the bottom up + // so we'll need to track the previous 2 numbers at each step + int prevPrev = 0; // 0th fibonacci + int prev = 1; // 1st fibonacci + int current = 0; // Declare and initialize current + + for (int i = 1; i < n; i++) { + + // Iteration 1: current = 2nd fibonacci + // Iteration 2: current = 3rd fibonacci + // Iteration 3: current = 4th fibonacci + // To get nth fibonacci ... do n-1 iterations. + current = prev + prevPrev; + prevPrev = prev; + prev = current; + } + + return current; + } + + /** + * Calculates the nth Fibonacci number. + * Works with n <= 67 + * + * @param n the nth number in the Fibonacci sequence to calculate + * @return the nth Fibonacci number + */ + public static double fibonacciSimpleBinetFormula(int n) { + if (n == 0) { + return 0; + } + final double phi = (1 + Math.sqrt(5)) / 2; + final double sqrt5 = Math.sqrt(5); + return ((Math.pow(phi, n) - Math.pow(-phi, -n)) / sqrt5); + } + + /** + * Calculates the nth Fibonacci number. + * Works with n <= 71 + * + * @param n the nth number in the Fibonacci sequence to calculate + * @return the nth Fibonacci number + */ + public static BigInteger fibonacciBinetFormula(int n) { + if (n == 0) { + return new BigInteger("0"); + } + if (n == 1 || n == 2) { + return new BigInteger("1"); + } + if (n == 3) { + return new BigInteger("2"); + } + + final double phi = (1 + Math.sqrt(5)) / 2; + BigDecimal sqrt5 = BigDecimal.valueOf(Math.sqrt(5)); + + BigDecimal pow = BigDecimal.valueOf(Math.pow(phi, n)); + BigDecimal powInvrt = BigDecimal.valueOf(Math.pow(-phi, -n)); + return pow.subtract(powInvrt).divide(sqrt5, RoundingMode.HALF_UP).toBigInteger(); + } + +} diff --git a/src/main/java/io/github/diegourban/algorithms/math/Statistics.java b/src/main/java/io/github/diegourban/algorithms/math/Statistics.java index f70e730..19eebaf 100644 --- a/src/main/java/io/github/diegourban/algorithms/math/Statistics.java +++ b/src/main/java/io/github/diegourban/algorithms/math/Statistics.java @@ -11,13 +11,13 @@ public class Statistics { * @param data * @return */ - public static double mean(int[] data) { + public static double mean(double[] data) { if (data.length == 0) { return 0.0; } double sum = 0.0; - for (int value : data) { + for (double value : data) { sum += value; } @@ -30,7 +30,7 @@ public static double mean(int[] data) { * @param data * @return */ - public static double median(int[] data) { + public static double median(double[] data) { Arrays.sort(data); final int middle = data.length / 2; @@ -48,8 +48,8 @@ public static double median(int[] data) { * @param data * @return */ - public static int mode(int[] data) { - int maxValue = 0; + public static double mode(double[] data) { + double maxValue = 0; int maxCount = 0; for (int i = 0; i < data.length; ++i) { @@ -74,11 +74,11 @@ public static int mode(int[] data) { * @param data * @return the Average Absolute Deviation from data */ - public static double meanAbsoluteDeviation(int[] data) { + public static double meanAbsoluteDeviation(double[] data) { double mean = mean(data); double sum = 0; - for (int value : data) { + for (double value : data) { sum += Math.abs(value - mean); } diff --git a/src/test/java/io/github/diegourban/algorithms/math/AlgebraTest.java b/src/test/java/io/github/diegourban/algorithms/math/AlgebraTest.java deleted file mode 100644 index 734120d..0000000 --- a/src/test/java/io/github/diegourban/algorithms/math/AlgebraTest.java +++ /dev/null @@ -1,219 +0,0 @@ -package io.github.diegourban.algorithms.math; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -import java.math.BigInteger; -import java.util.List; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.within; - -class AlgebraTest { - - @Test - void shouldCreateInstance() { - assertThat(new Algebra()).isNotNull(); - } - - @Test - void shouldFindLeastCommonMultiplesOfTwoNumbers() { - assertThat(Algebra.lcm(1, 2)).isEqualTo(2); - assertThat(Algebra.lcm(2, 3)).isEqualTo(6); - assertThat(Algebra.lcm(4, 2)).isEqualTo(4); - assertThat(Algebra.lcm(2, 4)).isEqualTo(4); - assertThat(Algebra.lcm(2, 5)).isEqualTo(10); - assertThat(Algebra.lcm(7, 11)).isEqualTo(77); - assertThat(Algebra.lcm(10, 100)).isEqualTo(100); - assertThat(Algebra.lcm(13, 67)).isEqualTo(871); - } - - @Test - void shouldFindLeastCommonMultiplesOfList() { - assertThat(Algebra.lcm(new long[]{1, 2, 3})).isEqualTo(6); - assertThat(Algebra.lcm(new long[]{2, 4, 6})).isEqualTo(12); - assertThat(Algebra.lcm(new long[]{7, 11, 13})).isEqualTo(1001); - } - - @Test - void shouldCalculateGreatestCommonDivisorOfTwoNumbersRecursively() { - assertThat(Algebra.gcdRecursive(2, 3)).isOne(); - assertThat(Algebra.gcdRecursive(2, 2)).isEqualTo(2); - assertThat(Algebra.gcdRecursive(2, 5)).isOne(); - assertThat(Algebra.gcdRecursive(7, 11)).isOne(); - assertThat(Algebra.gcdRecursive(412, 124)).isEqualTo(4); - } - - @Test - void shouldCalculateGreatestCommonDivisorOfTwoNumbersWithLoop() { - assertThat(Algebra.gcdLoop(2, 3)).isOne(); - assertThat(Algebra.gcdLoop(2, 2)).isEqualTo(2); - assertThat(Algebra.gcdLoop(2, 5)).isOne(); - assertThat(Algebra.gcdLoop(7, 11)).isOne(); - assertThat(Algebra.gcdLoop(412, 124)).isEqualTo(4); - } - - @Test - void greatestCommonDivisorShouldProduceSameResultWithLoopAndRecursion() { - assertThat(Algebra.gcdRecursive(2, 3)).isEqualTo(Algebra.gcdLoop(2, 3)); - assertThat(Algebra.gcdRecursive(5, 2)).isEqualTo(Algebra.gcdLoop(5, 2)); - assertThat(Algebra.gcdLoop(123, 321)).isEqualTo(Algebra.gcdRecursive(123, 321)); - assertThat(Algebra.gcdLoop(60, 12)).isEqualTo(Algebra.gcdRecursive(12, 60)); - } - - @Test - void shouldHandlePrimeNumbers() { - assertThat(Algebra.isPrime(2)).isTrue(); - assertThat(Algebra.isPrime(3)).isTrue(); - assertThat(Algebra.isPrime(5)).isTrue(); - assertThat(Algebra.isPrime(7)).isTrue(); - assertThat(Algebra.isPrime(11)).isTrue(); - assertThat(Algebra.isPrime(13)).isTrue(); - assertThat(Algebra.isPrime(17)).isTrue(); - assertThat(Algebra.isPrime(19)).isTrue(); - assertThat(Algebra.isPrime(23)).isTrue(); - assertThat(Algebra.isPrime(29)).isTrue(); - assertThat(Algebra.isPrime(31)).isTrue(); - assertThat(Algebra.isPrime(37)).isTrue(); - assertThat(Algebra.isPrime(41)).isTrue(); - assertThat(Algebra.isPrime(43)).isTrue(); - assertThat(Algebra.isPrime(47)).isTrue(); - assertThat(Algebra.isPrime(7919)).isTrue(); - } - - @Test - @Timeout(1) - void shouldHandlePrimalityNearIntegerMaxValue() { - assertThat(Algebra.isPrime(Integer.MAX_VALUE)).isTrue(); // 2^31 - 1 is a Mersenne prime - assertThat(Algebra.isPrime(46337 * 46337)).isFalse(); // square of the largest prime below sqrt(MAX_VALUE) - } - - @Test - void shouldHandleNotPrimeNumbers() { - assertThat(Algebra.isPrime(1)).isFalse(); - assertThat(Algebra.isPrime(4)).isFalse(); - assertThat(Algebra.isPrime(6)).isFalse(); - assertThat(Algebra.isPrime(8)).isFalse(); - assertThat(Algebra.isPrime(9)).isFalse(); - assertThat(Algebra.isPrime(10)).isFalse(); - assertThat(Algebra.isPrime(12)).isFalse(); - assertThat(Algebra.isPrime(14)).isFalse(); - assertThat(Algebra.isPrime(15)).isFalse(); - assertThat(Algebra.isPrime(16)).isFalse(); - assertThat(Algebra.isPrime(18)).isFalse(); - assertThat(Algebra.isPrime(20)).isFalse(); - assertThat(Algebra.isPrime(21)).isFalse(); - assertThat(Algebra.isPrime(22)).isFalse(); - assertThat(Algebra.isPrime(24)).isFalse(); - assertThat(Algebra.isPrime(25)).isFalse(); - assertThat(Algebra.isPrime(26)).isFalse(); - assertThat(Algebra.isPrime(27)).isFalse(); - assertThat(Algebra.isPrime(28)).isFalse(); - assertThat(Algebra.isPrime(30)).isFalse(); - assertThat(Algebra.isPrime(32)).isFalse(); - assertThat(Algebra.isPrime(33)).isFalse(); - assertThat(Algebra.isPrime(34)).isFalse(); - assertThat(Algebra.isPrime(35)).isFalse(); - assertThat(Algebra.isPrime(36)).isFalse(); - assertThat(Algebra.isPrime(38)).isFalse(); - assertThat(Algebra.isPrime(39)).isFalse(); - assertThat(Algebra.isPrime(40)).isFalse(); - assertThat(Algebra.isPrime(42)).isFalse(); - assertThat(Algebra.isPrime(44)).isFalse(); - assertThat(Algebra.isPrime(45)).isFalse(); - assertThat(Algebra.isPrime(46)).isFalse(); - assertThat(Algebra.isPrime(48)).isFalse(); - assertThat(Algebra.isPrime(49)).isFalse(); - assertThat(Algebra.isPrime(50)).isFalse(); - assertThat(Algebra.isPrime(7918)).isFalse(); - assertThat(Algebra.isPrime(7920)).isFalse(); - } - - @Test - void shouldListPrimeFactorsOfEvenNumber() { - List primeFactors = Algebra.primeFactorsList(10); - assertThat(primeFactors).hasSize(2); - assertThat(primeFactors.get(0).intValue()).isEqualTo(2); - assertThat(primeFactors.get(1).intValue()).isEqualTo(5); - } - - @Test - void shouldListPrimeFactorsOfOddNumber() { - List primeFactors = Algebra.primeFactorsList(49); - assertThat(primeFactors).hasSize(2); - assertThat(primeFactors.get(0).intValue()).isEqualTo(7); - assertThat(primeFactors.get(1).intValue()).isEqualTo(7); - } - - @Test - void shouldListPrimeFactorsOfPrimeNumber() { - List primeFactors = Algebra.primeFactorsList(13); - assertThat(primeFactors).hasSize(1); - assertThat(primeFactors.get(0).intValue()).isEqualTo(13); - } - - @Test - void shouldCreateSetOfPrimeFactorsOfEvenNumber() { - Set primeFactors = Algebra.primeFactorsSet(12); - assertThat(primeFactors) - .hasSize(2) - .contains(2) - .contains(3); - } - - @Test - void shouldCreateSetOfPrimeFactorsOfOddNumber() { - Set primeFactors = Algebra.primeFactorsSet(49); - assertThat(primeFactors) - .hasSize(1) - .contains(7); - } - - @Test - void shouldCreateSetOfPrimeFactorsOfPrimeNumber() { - Set primeFactors = Algebra.primeFactorsSet(7); - assertThat(primeFactors) - .hasSize(1) - .contains(7); - } - - @Test - void shouldCalculateFibonacci() { - assertThat(Algebra.fibonacci(0)).isZero(); - assertThat(Algebra.fibonacci(1)).isOne(); - assertThat(Algebra.fibonacci(2)).isOne(); - assertThat(Algebra.fibonacci(3)).isEqualTo(2); - assertThat(Algebra.fibonacci(4)).isEqualTo(3); - assertThat(Algebra.fibonacci(5)).isEqualTo(5); - assertThat(Algebra.fibonacci(11)).isEqualTo(89); - assertThat(Algebra.fibonacci(46)).isEqualTo(1836311903); - } - - @Test - void shouldCalculateFibonacciWithSimpleBinetFormula() { - assertThat(Algebra.fibonacciSimpleBinetFormula(0)).isCloseTo(0, within(0.1)); - assertThat(Algebra.fibonacciSimpleBinetFormula(1)).isCloseTo(1, within(0.1)); - assertThat(Algebra.fibonacciSimpleBinetFormula(2)).isCloseTo(1, within(0.1)); - assertThat(Algebra.fibonacciSimpleBinetFormula(3)).isCloseTo(2, within(0.1)); - assertThat(Algebra.fibonacciSimpleBinetFormula(4)).isCloseTo(3, within(0.1)); - assertThat(Algebra.fibonacciSimpleBinetFormula(5)).isCloseTo(5, within(0.1)); - assertThat(Algebra.fibonacciSimpleBinetFormula(11)).isCloseTo(89, within(0.1)); - assertThat(Algebra.fibonacciSimpleBinetFormula(50)).isCloseTo(12586269025d, within(0.1)); - assertThat(Algebra.fibonacciSimpleBinetFormula(67)).isCloseTo(44945570212853d, within(0.1)); - } - - @Test - void shouldCalculateFibonacciWithBinetFormula() { - assertThat(Algebra.fibonacciBinetFormula(0)).isEqualTo(new BigInteger("0")); - assertThat(Algebra.fibonacciBinetFormula(1)).isEqualTo(new BigInteger("1")); - assertThat(Algebra.fibonacciBinetFormula(2)).isEqualTo(new BigInteger("1")); - assertThat(Algebra.fibonacciBinetFormula(3)).isEqualTo(new BigInteger("2")); - assertThat(Algebra.fibonacciBinetFormula(4)).isEqualTo(new BigInteger("3")); - assertThat(Algebra.fibonacciBinetFormula(5)).isEqualTo(new BigInteger("5")); - assertThat(Algebra.fibonacciBinetFormula(11)).isEqualTo(new BigInteger("89")); - assertThat(Algebra.fibonacciBinetFormula(50)).isEqualTo(new BigInteger("12586269025")); - assertThat(Algebra.fibonacciBinetFormula(71)).isEqualTo(new BigInteger("308061521170129")); - } - -} \ No newline at end of file diff --git a/src/test/java/io/github/diegourban/algorithms/math/NumberTheoryPropertyTest.java b/src/test/java/io/github/diegourban/algorithms/math/NumberTheoryPropertyTest.java new file mode 100644 index 0000000..1ddddc9 --- /dev/null +++ b/src/test/java/io/github/diegourban/algorithms/math/NumberTheoryPropertyTest.java @@ -0,0 +1,72 @@ +package io.github.diegourban.algorithms.math; + +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.constraints.IntRange; +import net.jqwik.api.constraints.LongRange; +import net.jqwik.api.constraints.Size; + +import java.util.HashSet; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class NumberTheoryPropertyTest { + + @Property + void bothGcdStrategiesAgree(@ForAll @LongRange(min = 0) long a, @ForAll @LongRange(min = 0) long b) { + assertThat(NumberTheory.gcdLoop(a, b)).isEqualTo(NumberTheory.gcdRecursive(a, b)); + } + + @Property + void gcdDividesBothNumbers(@ForAll @LongRange(min = 1) long a, @ForAll @LongRange(min = 1) long b) { + var gcd = NumberTheory.gcdLoop(a, b); + + assertThat(a % gcd).isZero(); + assertThat(b % gcd).isZero(); + } + + @Property + void lcmTimesGcdIsTheProduct(@ForAll @LongRange(min = 1, max = 1_000_000) long a, + @ForAll @LongRange(min = 1, max = 1_000_000) long b) { + assertThat(NumberTheory.lcm(a, b) * NumberTheory.gcdLoop(a, b)).isEqualTo(a * b); + } + + @Property + void lcmOfAListIsAMultipleOfEveryElement(@ForAll @Size(min = 1, max = 6) List<@LongRange(min = 1, max = 1_000) Long> numbers) { + var lcm = NumberTheory.lcm(numbers.stream().mapToLong(Long::longValue).toArray()); + + assertThat(numbers).allSatisfy(n -> assertThat(lcm % n).isZero()); + } + + @Property + void isPrimeAgreesWithTrialDivision(@ForAll @IntRange(min = -10, max = 200_000) int n) { + assertThat(NumberTheory.isPrime(n)).isEqualTo(isPrimeByTrialDivision(n)); + } + + @Property + void primeFactorsMultiplyBackToTheNumber(@ForAll @IntRange(min = 2, max = 10_000_000) int n) { + var factors = NumberTheory.primeFactorsList(n); + + assertThat(factors.stream().mapToLong(Integer::longValue).reduce(1, (x, y) -> x * y)).isEqualTo(n); + assertThat(factors).allSatisfy(f -> assertThat(isPrimeByTrialDivision(f)).isTrue()); + assertThat(factors).isSorted(); + } + + @Property + void primeFactorsSetHoldsTheDistinctFactors(@ForAll @IntRange(min = 2, max = 10_000_000) int n) { + assertThat(NumberTheory.primeFactorsSet(n)).isEqualTo(new HashSet<>(NumberTheory.primeFactorsList(n))); + } + + private static boolean isPrimeByTrialDivision(int n) { + if (n < 2) { + return false; + } + for (var d = 2; (long) d * d <= n; d++) { + if (n % d == 0) { + return false; + } + } + return true; + } +} diff --git a/src/test/java/io/github/diegourban/algorithms/math/NumberTheoryTest.java b/src/test/java/io/github/diegourban/algorithms/math/NumberTheoryTest.java new file mode 100644 index 0000000..83c574f --- /dev/null +++ b/src/test/java/io/github/diegourban/algorithms/math/NumberTheoryTest.java @@ -0,0 +1,179 @@ +package io.github.diegourban.algorithms.math; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class NumberTheoryTest { + + @Test + void shouldCreateInstance() { + assertThat(new NumberTheory()).isNotNull(); + } + + @Test + void shouldFindLeastCommonMultiplesOfTwoNumbers() { + assertThat(NumberTheory.lcm(1, 2)).isEqualTo(2); + assertThat(NumberTheory.lcm(2, 3)).isEqualTo(6); + assertThat(NumberTheory.lcm(4, 2)).isEqualTo(4); + assertThat(NumberTheory.lcm(2, 4)).isEqualTo(4); + assertThat(NumberTheory.lcm(2, 5)).isEqualTo(10); + assertThat(NumberTheory.lcm(7, 11)).isEqualTo(77); + assertThat(NumberTheory.lcm(10, 100)).isEqualTo(100); + assertThat(NumberTheory.lcm(13, 67)).isEqualTo(871); + } + + @Test + void shouldFindLeastCommonMultiplesOfList() { + assertThat(NumberTheory.lcm(new long[]{1, 2, 3})).isEqualTo(6); + assertThat(NumberTheory.lcm(new long[]{2, 4, 6})).isEqualTo(12); + assertThat(NumberTheory.lcm(new long[]{7, 11, 13})).isEqualTo(1001); + } + + @Test + void shouldCalculateGreatestCommonDivisorOfTwoNumbersRecursively() { + assertThat(NumberTheory.gcdRecursive(2, 3)).isOne(); + assertThat(NumberTheory.gcdRecursive(2, 2)).isEqualTo(2); + assertThat(NumberTheory.gcdRecursive(2, 5)).isOne(); + assertThat(NumberTheory.gcdRecursive(7, 11)).isOne(); + assertThat(NumberTheory.gcdRecursive(412, 124)).isEqualTo(4); + } + + @Test + void shouldCalculateGreatestCommonDivisorOfTwoNumbersWithLoop() { + assertThat(NumberTheory.gcdLoop(2, 3)).isOne(); + assertThat(NumberTheory.gcdLoop(2, 2)).isEqualTo(2); + assertThat(NumberTheory.gcdLoop(2, 5)).isOne(); + assertThat(NumberTheory.gcdLoop(7, 11)).isOne(); + assertThat(NumberTheory.gcdLoop(412, 124)).isEqualTo(4); + } + + @Test + void greatestCommonDivisorShouldProduceSameResultWithLoopAndRecursion() { + assertThat(NumberTheory.gcdRecursive(2, 3)).isEqualTo(NumberTheory.gcdLoop(2, 3)); + assertThat(NumberTheory.gcdRecursive(5, 2)).isEqualTo(NumberTheory.gcdLoop(5, 2)); + assertThat(NumberTheory.gcdLoop(123, 321)).isEqualTo(NumberTheory.gcdRecursive(123, 321)); + assertThat(NumberTheory.gcdLoop(60, 12)).isEqualTo(NumberTheory.gcdRecursive(12, 60)); + } + + @Test + void shouldHandlePrimeNumbers() { + assertThat(NumberTheory.isPrime(2)).isTrue(); + assertThat(NumberTheory.isPrime(3)).isTrue(); + assertThat(NumberTheory.isPrime(5)).isTrue(); + assertThat(NumberTheory.isPrime(7)).isTrue(); + assertThat(NumberTheory.isPrime(11)).isTrue(); + assertThat(NumberTheory.isPrime(13)).isTrue(); + assertThat(NumberTheory.isPrime(17)).isTrue(); + assertThat(NumberTheory.isPrime(19)).isTrue(); + assertThat(NumberTheory.isPrime(23)).isTrue(); + assertThat(NumberTheory.isPrime(29)).isTrue(); + assertThat(NumberTheory.isPrime(31)).isTrue(); + assertThat(NumberTheory.isPrime(37)).isTrue(); + assertThat(NumberTheory.isPrime(41)).isTrue(); + assertThat(NumberTheory.isPrime(43)).isTrue(); + assertThat(NumberTheory.isPrime(47)).isTrue(); + assertThat(NumberTheory.isPrime(7919)).isTrue(); + } + + @Test + @Timeout(1) + void shouldHandlePrimalityNearIntegerMaxValue() { + assertThat(NumberTheory.isPrime(Integer.MAX_VALUE)).isTrue(); // 2^31 - 1 is a Mersenne prime + assertThat(NumberTheory.isPrime(46337 * 46337)).isFalse(); // square of the largest prime below sqrt(MAX_VALUE) + } + + @Test + void shouldHandleNotPrimeNumbers() { + assertThat(NumberTheory.isPrime(1)).isFalse(); + assertThat(NumberTheory.isPrime(4)).isFalse(); + assertThat(NumberTheory.isPrime(6)).isFalse(); + assertThat(NumberTheory.isPrime(8)).isFalse(); + assertThat(NumberTheory.isPrime(9)).isFalse(); + assertThat(NumberTheory.isPrime(10)).isFalse(); + assertThat(NumberTheory.isPrime(12)).isFalse(); + assertThat(NumberTheory.isPrime(14)).isFalse(); + assertThat(NumberTheory.isPrime(15)).isFalse(); + assertThat(NumberTheory.isPrime(16)).isFalse(); + assertThat(NumberTheory.isPrime(18)).isFalse(); + assertThat(NumberTheory.isPrime(20)).isFalse(); + assertThat(NumberTheory.isPrime(21)).isFalse(); + assertThat(NumberTheory.isPrime(22)).isFalse(); + assertThat(NumberTheory.isPrime(24)).isFalse(); + assertThat(NumberTheory.isPrime(25)).isFalse(); + assertThat(NumberTheory.isPrime(26)).isFalse(); + assertThat(NumberTheory.isPrime(27)).isFalse(); + assertThat(NumberTheory.isPrime(28)).isFalse(); + assertThat(NumberTheory.isPrime(30)).isFalse(); + assertThat(NumberTheory.isPrime(32)).isFalse(); + assertThat(NumberTheory.isPrime(33)).isFalse(); + assertThat(NumberTheory.isPrime(34)).isFalse(); + assertThat(NumberTheory.isPrime(35)).isFalse(); + assertThat(NumberTheory.isPrime(36)).isFalse(); + assertThat(NumberTheory.isPrime(38)).isFalse(); + assertThat(NumberTheory.isPrime(39)).isFalse(); + assertThat(NumberTheory.isPrime(40)).isFalse(); + assertThat(NumberTheory.isPrime(42)).isFalse(); + assertThat(NumberTheory.isPrime(44)).isFalse(); + assertThat(NumberTheory.isPrime(45)).isFalse(); + assertThat(NumberTheory.isPrime(46)).isFalse(); + assertThat(NumberTheory.isPrime(48)).isFalse(); + assertThat(NumberTheory.isPrime(49)).isFalse(); + assertThat(NumberTheory.isPrime(50)).isFalse(); + assertThat(NumberTheory.isPrime(7918)).isFalse(); + assertThat(NumberTheory.isPrime(7920)).isFalse(); + } + + @Test + void shouldListPrimeFactorsOfEvenNumber() { + List primeFactors = NumberTheory.primeFactorsList(10); + assertThat(primeFactors).hasSize(2); + assertThat(primeFactors.get(0).intValue()).isEqualTo(2); + assertThat(primeFactors.get(1).intValue()).isEqualTo(5); + } + + @Test + void shouldListPrimeFactorsOfOddNumber() { + List primeFactors = NumberTheory.primeFactorsList(49); + assertThat(primeFactors).hasSize(2); + assertThat(primeFactors.get(0).intValue()).isEqualTo(7); + assertThat(primeFactors.get(1).intValue()).isEqualTo(7); + } + + @Test + void shouldListPrimeFactorsOfPrimeNumber() { + List primeFactors = NumberTheory.primeFactorsList(13); + assertThat(primeFactors).hasSize(1); + assertThat(primeFactors.get(0).intValue()).isEqualTo(13); + } + + @Test + void shouldCreateSetOfPrimeFactorsOfEvenNumber() { + Set primeFactors = NumberTheory.primeFactorsSet(12); + assertThat(primeFactors) + .hasSize(2) + .contains(2) + .contains(3); + } + + @Test + void shouldCreateSetOfPrimeFactorsOfOddNumber() { + Set primeFactors = NumberTheory.primeFactorsSet(49); + assertThat(primeFactors) + .hasSize(1) + .contains(7); + } + + @Test + void shouldCreateSetOfPrimeFactorsOfPrimeNumber() { + Set primeFactors = NumberTheory.primeFactorsSet(7); + assertThat(primeFactors) + .hasSize(1) + .contains(7); + } + +} \ No newline at end of file diff --git a/src/test/java/io/github/diegourban/algorithms/math/SequencesPropertyTest.java b/src/test/java/io/github/diegourban/algorithms/math/SequencesPropertyTest.java new file mode 100644 index 0000000..a843a43 --- /dev/null +++ b/src/test/java/io/github/diegourban/algorithms/math/SequencesPropertyTest.java @@ -0,0 +1,42 @@ +package io.github.diegourban.algorithms.math; + +import net.jqwik.api.ForAll; +import net.jqwik.api.GenerationMode; +import net.jqwik.api.Property; +import net.jqwik.api.constraints.IntRange; + +import java.math.BigInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Checks each Fibonacci variant against an exact reference, over the range its Javadoc says it supports. + */ +class SequencesPropertyTest { + + @Property(generation = GenerationMode.EXHAUSTIVE) + void fibonacciIsExactUpTo46(@ForAll @IntRange(min = 0, max = 46) int n) { + assertThat(BigInteger.valueOf(Sequences.fibonacci(n))).isEqualTo(exactFibonacci(n)); + } + + @Property(generation = GenerationMode.EXHAUSTIVE) + void simpleBinetFormulaRoundsToTheExactValueUpTo67(@ForAll @IntRange(min = 0, max = 67) int n) { + assertThat(BigInteger.valueOf(Math.round(Sequences.fibonacciSimpleBinetFormula(n)))).isEqualTo(exactFibonacci(n)); + } + + @Property(generation = GenerationMode.EXHAUSTIVE) + void binetFormulaIsExactUpTo71(@ForAll @IntRange(min = 0, max = 71) int n) { + assertThat(Sequences.fibonacciBinetFormula(n)).isEqualTo(exactFibonacci(n)); + } + + private static BigInteger exactFibonacci(int n) { + var previous = BigInteger.ZERO; + var current = BigInteger.ONE; + for (var i = 0; i < n; i++) { + var next = previous.add(current); + previous = current; + current = next; + } + return previous; + } +} diff --git a/src/test/java/io/github/diegourban/algorithms/math/SequencesTest.java b/src/test/java/io/github/diegourban/algorithms/math/SequencesTest.java new file mode 100644 index 0000000..6f5728d --- /dev/null +++ b/src/test/java/io/github/diegourban/algorithms/math/SequencesTest.java @@ -0,0 +1,57 @@ +package io.github.diegourban.algorithms.math; + +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.within; + +class SequencesTest { + + @Test + void shouldCalculateFibonacci() { + assertThat(Sequences.fibonacci(0)).isZero(); + assertThat(Sequences.fibonacci(1)).isOne(); + assertThat(Sequences.fibonacci(2)).isOne(); + assertThat(Sequences.fibonacci(3)).isEqualTo(2); + assertThat(Sequences.fibonacci(4)).isEqualTo(3); + assertThat(Sequences.fibonacci(5)).isEqualTo(5); + assertThat(Sequences.fibonacci(11)).isEqualTo(89); + assertThat(Sequences.fibonacci(46)).isEqualTo(1836311903); + } + + @Test + void shouldCalculateFibonacciWithSimpleBinetFormula() { + assertThat(Sequences.fibonacciSimpleBinetFormula(0)).isCloseTo(0, within(0.1)); + assertThat(Sequences.fibonacciSimpleBinetFormula(1)).isCloseTo(1, within(0.1)); + assertThat(Sequences.fibonacciSimpleBinetFormula(2)).isCloseTo(1, within(0.1)); + assertThat(Sequences.fibonacciSimpleBinetFormula(3)).isCloseTo(2, within(0.1)); + assertThat(Sequences.fibonacciSimpleBinetFormula(4)).isCloseTo(3, within(0.1)); + assertThat(Sequences.fibonacciSimpleBinetFormula(5)).isCloseTo(5, within(0.1)); + assertThat(Sequences.fibonacciSimpleBinetFormula(11)).isCloseTo(89, within(0.1)); + assertThat(Sequences.fibonacciSimpleBinetFormula(50)).isCloseTo(12586269025d, within(0.1)); + assertThat(Sequences.fibonacciSimpleBinetFormula(67)).isCloseTo(44945570212853d, within(0.1)); + } + + @Test + void shouldCalculateFibonacciWithBinetFormula() { + assertThat(Sequences.fibonacciBinetFormula(0)).isEqualTo(new BigInteger("0")); + assertThat(Sequences.fibonacciBinetFormula(1)).isEqualTo(new BigInteger("1")); + assertThat(Sequences.fibonacciBinetFormula(2)).isEqualTo(new BigInteger("1")); + assertThat(Sequences.fibonacciBinetFormula(3)).isEqualTo(new BigInteger("2")); + assertThat(Sequences.fibonacciBinetFormula(4)).isEqualTo(new BigInteger("3")); + assertThat(Sequences.fibonacciBinetFormula(5)).isEqualTo(new BigInteger("5")); + assertThat(Sequences.fibonacciBinetFormula(11)).isEqualTo(new BigInteger("89")); + assertThat(Sequences.fibonacciBinetFormula(50)).isEqualTo(new BigInteger("12586269025")); + assertThat(Sequences.fibonacciBinetFormula(71)).isEqualTo(new BigInteger("308061521170129")); + } + + @Test + void shouldRejectNegativeFibonacciIndex() { + assertThatIllegalArgumentException() + .isThrownBy(() -> Sequences.fibonacci(-1)) + .withMessage("Index was negative. No such thing as a negative index in a series."); + } +} diff --git a/src/test/java/io/github/diegourban/algorithms/math/StatisticsPropertyTest.java b/src/test/java/io/github/diegourban/algorithms/math/StatisticsPropertyTest.java new file mode 100644 index 0000000..c3f7596 --- /dev/null +++ b/src/test/java/io/github/diegourban/algorithms/math/StatisticsPropertyTest.java @@ -0,0 +1,43 @@ +package io.github.diegourban.algorithms.math; + +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.constraints.DoubleRange; +import net.jqwik.api.constraints.Size; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +class StatisticsPropertyTest { + + @Property + void meanLiesBetweenMinAndMax(@ForAll @Size(min = 1, max = 50) List<@DoubleRange(min = -1e6, max = 1e6) Double> values) { + var data = values.stream().mapToDouble(Double::doubleValue).toArray(); + var min = Arrays.stream(data).min().orElseThrow(); + var max = Arrays.stream(data).max().orElseThrow(); + + assertThat(Statistics.mean(data)).isBetween(min - 1e-6, max + 1e-6); + } + + @Property + void medianIsTheMiddleOfTheSortedData(@ForAll @Size(min = 1, max = 50) List<@DoubleRange(min = -1e6, max = 1e6) Double> values) { + var data = values.stream().mapToDouble(Double::doubleValue).toArray(); + var sorted = data.clone(); + Arrays.sort(sorted); + var middle = sorted.length / 2; + var expected = sorted.length % 2 == 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2.0; + + assertThat(Statistics.median(data.clone())).isCloseTo(expected, within(1e-9)); + } + + @Property + void modeIsOneOfTheValues(@ForAll @Size(min = 1, max = 50) List<@DoubleRange(min = -5, max = 5) Double> values) { + var data = values.stream().mapToDouble(Double::doubleValue).toArray(); + var rounded = Arrays.stream(data).map(Math::rint).toArray(); // few distinct values, so repeats are likely + + assertThat(rounded).contains(Statistics.mode(rounded)); + } +} diff --git a/src/test/java/io/github/diegourban/algorithms/math/StatisticsTest.java b/src/test/java/io/github/diegourban/algorithms/math/StatisticsTest.java index 1c2e4cd..ce79f68 100644 --- a/src/test/java/io/github/diegourban/algorithms/math/StatisticsTest.java +++ b/src/test/java/io/github/diegourban/algorithms/math/StatisticsTest.java @@ -15,37 +15,37 @@ void shouldCreateInstance() { @Test void shouldCalculateMean() { - assertThat(Statistics.mean(new int[]{})).isCloseTo(0.0, within(0.1)); - assertThat(Statistics.mean(new int[]{1})).isCloseTo(1.0, within(0.1)); - assertThat(Statistics.mean(new int[]{1, 0})).isCloseTo(0.5, within(0.1)); - assertThat(Statistics.mean(new int[]{3, 2, 1})).isCloseTo(2.0, within(0.1)); - assertThat(Statistics.mean(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})).isCloseTo(5.5, within(0.1)); - assertThat(Statistics.mean(new int[]{2, 2, 3, 4, 14})).isCloseTo(5.0, within(0.1)); + assertThat(Statistics.mean(new double[]{})).isCloseTo(0.0, within(0.1)); + assertThat(Statistics.mean(new double[]{1})).isCloseTo(1.0, within(0.1)); + assertThat(Statistics.mean(new double[]{1, 0})).isCloseTo(0.5, within(0.1)); + assertThat(Statistics.mean(new double[]{3, 2, 1})).isCloseTo(2.0, within(0.1)); + assertThat(Statistics.mean(new double[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})).isCloseTo(5.5, within(0.1)); + assertThat(Statistics.mean(new double[]{2, 2, 3, 4, 14})).isCloseTo(5.0, within(0.1)); } @Test void shouldCalculateMedian() { - assertThat(Statistics.median(new int[]{1})).isCloseTo(1.0, within(0.1)); - assertThat(Statistics.median(new int[]{1, 0})).isCloseTo(0.5, within(0.1)); - assertThat(Statistics.median(new int[]{3, 2, 1})).isCloseTo(2.0, within(0.1)); - assertThat(Statistics.median(new int[]{1, 2, 3})).isCloseTo(2.0, within(0.1)); - assertThat(Statistics.median(new int[]{2, 1, 3})).isCloseTo(2.0, within(0.1)); - assertThat(Statistics.median(new int[]{3, 1, 2})).isCloseTo(2.0, within(0.1)); - assertThat(Statistics.median(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})).isCloseTo(5.5, within(0.1)); - assertThat(Statistics.median(new int[]{2, 2, 3, 4, 14})).isCloseTo(3.0, within(0.1)); + assertThat(Statistics.median(new double[]{1})).isCloseTo(1.0, within(0.1)); + assertThat(Statistics.median(new double[]{1, 0})).isCloseTo(0.5, within(0.1)); + assertThat(Statistics.median(new double[]{3, 2, 1})).isCloseTo(2.0, within(0.1)); + assertThat(Statistics.median(new double[]{1, 2, 3})).isCloseTo(2.0, within(0.1)); + assertThat(Statistics.median(new double[]{2, 1, 3})).isCloseTo(2.0, within(0.1)); + assertThat(Statistics.median(new double[]{3, 1, 2})).isCloseTo(2.0, within(0.1)); + assertThat(Statistics.median(new double[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})).isCloseTo(5.5, within(0.1)); + assertThat(Statistics.median(new double[]{2, 2, 3, 4, 14})).isCloseTo(3.0, within(0.1)); } @Test void shouldCalculateMode() { - assertThat(Statistics.mode(new int[]{1})).isOne(); - assertThat(Statistics.mode(new int[]{1, 0, 1})).isOne(); - assertThat(Statistics.mode(new int[]{1, 2, 2, 3, 3, 3, 4, 4, 5, 4, 4})).isEqualTo(4); - assertThat(Statistics.mode(new int[]{1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 5, 4, 4, 5})).isEqualTo(5); - assertThat(Statistics.mode(new int[]{2, 2, 3, 4, 14})).isEqualTo(2); + assertThat(Statistics.mode(new double[]{1})).isEqualTo(1.0); + assertThat(Statistics.mode(new double[]{1, 0, 1})).isEqualTo(1.0); + assertThat(Statistics.mode(new double[]{1, 2, 2, 3, 3, 3, 4, 4, 5, 4, 4})).isEqualTo(4.0); + assertThat(Statistics.mode(new double[]{1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 5, 4, 4, 5})).isEqualTo(5.0); + assertThat(Statistics.mode(new double[]{2, 2, 3, 4, 14})).isEqualTo(2.0); } @Test void shouldCalculateAverageAbsoluteDeviation() { - assertThat(Statistics.meanAbsoluteDeviation(new int[]{2, 2, 3, 4, 14})).isCloseTo(3.6, within(0.1)); + assertThat(Statistics.meanAbsoluteDeviation(new double[]{2, 2, 3, 4, 14})).isCloseTo(3.6, within(0.1)); } } diff --git a/src/test/java/io/github/diegourban/algorithms/problems/math/CollectableCardsTest.java b/src/test/java/io/github/diegourban/algorithms/problems/math/CollectableCardsTest.java index 9ba54e8..46456ac 100644 --- a/src/test/java/io/github/diegourban/algorithms/problems/math/CollectableCardsTest.java +++ b/src/test/java/io/github/diegourban/algorithms/problems/math/CollectableCardsTest.java @@ -1,7 +1,7 @@ package io.github.diegourban.algorithms.problems.math; import io.github.diegourban.algorithms.io.Reader; -import io.github.diegourban.algorithms.math.Algebra; +import io.github.diegourban.algorithms.math.NumberTheory; import io.github.diegourban.algorithms.testing.SampleLoader; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -25,7 +25,7 @@ class CollectableCardsTest { private static final SampleLoader SAMPLES = new SampleLoader("problems/math/collectable-cards"); static long solve(int f1, int f2) { - return Algebra.gcdLoop(f1, f2); + return NumberTheory.gcdLoop(f1, f2); } static String run(InputStream in) throws IOException { diff --git a/src/test/java/io/github/diegourban/algorithms/problems/math/CommonDenominatorsTest.java b/src/test/java/io/github/diegourban/algorithms/problems/math/CommonDenominatorsTest.java index 0eaa2c0..8fa797e 100644 --- a/src/test/java/io/github/diegourban/algorithms/problems/math/CommonDenominatorsTest.java +++ b/src/test/java/io/github/diegourban/algorithms/problems/math/CommonDenominatorsTest.java @@ -1,6 +1,6 @@ package io.github.diegourban.algorithms.problems.math; -import io.github.diegourban.algorithms.math.Algebra; +import io.github.diegourban.algorithms.math.NumberTheory; import org.junit.jupiter.api.Test; import java.util.Arrays; @@ -21,7 +21,7 @@ static String solve(long[][] fractions) { if (fractions.length == 0) { return ""; } - var lcm = Algebra.lcm(Arrays.stream(fractions).mapToLong(f -> f[1]).toArray()); + var lcm = NumberTheory.lcm(Arrays.stream(fractions).mapToLong(f -> f[1]).toArray()); var result = new StringBuilder(); for (var f : fractions) { result.append('(').append(f[0] * (lcm / f[1])).append(',').append(lcm).append(')'); diff --git a/src/test/java/io/github/diegourban/algorithms/problems/math/PrimeNumberTest.java b/src/test/java/io/github/diegourban/algorithms/problems/math/PrimeNumberTest.java index 61ed1b4..3a2a062 100644 --- a/src/test/java/io/github/diegourban/algorithms/problems/math/PrimeNumberTest.java +++ b/src/test/java/io/github/diegourban/algorithms/problems/math/PrimeNumberTest.java @@ -1,6 +1,6 @@ package io.github.diegourban.algorithms.problems.math; -import io.github.diegourban.algorithms.math.Algebra; +import io.github.diegourban.algorithms.math.NumberTheory; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -16,7 +16,7 @@ class PrimeNumberTest { static boolean solve(int x) { - return Algebra.isPrime(x); + return NumberTheory.isPrime(x); } @ParameterizedTest diff --git a/src/test/java/io/github/diegourban/algorithms/problems/math/PrimesInNumbersTest.java b/src/test/java/io/github/diegourban/algorithms/problems/math/PrimesInNumbersTest.java index b6af82e..30ee306 100644 --- a/src/test/java/io/github/diegourban/algorithms/problems/math/PrimesInNumbersTest.java +++ b/src/test/java/io/github/diegourban/algorithms/problems/math/PrimesInNumbersTest.java @@ -1,6 +1,6 @@ package io.github.diegourban.algorithms.problems.math; -import io.github.diegourban.algorithms.math.Algebra; +import io.github.diegourban.algorithms.math.NumberTheory; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -19,7 +19,7 @@ class PrimesInNumbersTest { static String solve(int n) { - var counts = Algebra.primeFactorsList(n).stream() + var counts = NumberTheory.primeFactorsList(n).stream() .collect(Collectors.groupingBy(Function.identity(), TreeMap::new, Collectors.counting())); var result = new StringBuilder(); counts.forEach((prime, k) -> { diff --git a/src/test/java/io/github/diegourban/algorithms/problems/math/RainfallTest.java b/src/test/java/io/github/diegourban/algorithms/problems/math/RainfallTest.java index b33f81b..f8d42df 100644 --- a/src/test/java/io/github/diegourban/algorithms/problems/math/RainfallTest.java +++ b/src/test/java/io/github/diegourban/algorithms/problems/math/RainfallTest.java @@ -1,5 +1,6 @@ package io.github.diegourban.algorithms.problems.math; +import io.github.diegourban.algorithms.math.Statistics; import org.junit.jupiter.api.Test; import java.util.Arrays; @@ -11,8 +12,7 @@ * Rainfall — Codewars (6 kyu). *

* Find the town's record line, parse its monthly values and compute the mean and population variance; - * an unknown town yields -1 for both. O(size of data) time. {@code Statistics.mean} is not reused as it - * only takes {@code int[]}. + * an unknown town yields -1 for both. O(size of data) time. The mean comes from the library's {@link Statistics#mean}. */ class RainfallTest { @@ -29,7 +29,7 @@ static Rainfall solve(String town, String data) { var values = Arrays.stream(line.substring(town.length() + 1).split(",")) .mapToDouble(month -> Double.parseDouble(month.split(" ")[1])) .toArray(); - var mean = Arrays.stream(values).average().orElseThrow(); + var mean = Statistics.mean(values); var variance = Arrays.stream(values).map(v -> (v - mean) * (v - mean)).average().orElseThrow(); return new Rainfall(mean, variance); }) diff --git a/src/test/java/io/github/diegourban/algorithms/problems/math/SumByFactorsTest.java b/src/test/java/io/github/diegourban/algorithms/problems/math/SumByFactorsTest.java index ceace7b..7dd6309 100644 --- a/src/test/java/io/github/diegourban/algorithms/problems/math/SumByFactorsTest.java +++ b/src/test/java/io/github/diegourban/algorithms/problems/math/SumByFactorsTest.java @@ -1,6 +1,6 @@ package io.github.diegourban.algorithms.problems.math; -import io.github.diegourban.algorithms.math.Algebra; +import io.github.diegourban.algorithms.math.NumberTheory; import org.junit.jupiter.api.Test; import java.util.TreeMap; @@ -20,7 +20,7 @@ class SumByFactorsTest { static String solve(int[] numbers) { var sums = new TreeMap(); for (var number : numbers) { - for (var prime : Algebra.primeFactorsSet(Math.abs(number))) { + for (var prime : NumberTheory.primeFactorsSet(Math.abs(number))) { sums.merge(prime, number, Integer::sum); } } diff --git a/src/test/java/io/github/diegourban/algorithms/problems/math/TdaRationalTest.java b/src/test/java/io/github/diegourban/algorithms/problems/math/TdaRationalTest.java index f03c60a..a4d40b6 100644 --- a/src/test/java/io/github/diegourban/algorithms/problems/math/TdaRationalTest.java +++ b/src/test/java/io/github/diegourban/algorithms/problems/math/TdaRationalTest.java @@ -1,6 +1,6 @@ package io.github.diegourban.algorithms.problems.math; -import io.github.diegourban.algorithms.math.Algebra; +import io.github.diegourban.algorithms.math.NumberTheory; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -32,7 +32,7 @@ static Result solve(Fraction a, char operator, Fraction b) { case '/' -> new Fraction(a.numerator() * b.denominator(), b.numerator() * a.denominator()); default -> throw new IllegalArgumentException("Invalid operator: " + operator); }; - var gcd = (int) Algebra.gcdLoop(Math.abs(raw.numerator()), Math.abs(raw.denominator())); + var gcd = (int) NumberTheory.gcdLoop(Math.abs(raw.numerator()), Math.abs(raw.denominator())); return new Result(raw, new Fraction(raw.numerator() / gcd, raw.denominator() / gcd)); } diff --git a/src/test/java/io/github/diegourban/algorithms/string/StringUtilsPropertyTest.java b/src/test/java/io/github/diegourban/algorithms/string/StringUtilsPropertyTest.java new file mode 100644 index 0000000..9f878ad --- /dev/null +++ b/src/test/java/io/github/diegourban/algorithms/string/StringUtilsPropertyTest.java @@ -0,0 +1,57 @@ +package io.github.diegourban.algorithms.string; + +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.constraints.AlphaChars; +import net.jqwik.api.constraints.NotEmpty; +import net.jqwik.api.constraints.StringLength; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Random; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Inputs are ASCII letters: both methods compare {@code char} by {@code char}, so the properties check their logic + * rather than Unicode case-mapping and surrogate-pair corner cases. + */ +class StringUtilsPropertyTest { + + @Property + void isPalindromeAgreesWithReversing(@ForAll @AlphaChars @NotEmpty @StringLength(max = 20) String s) { + var reversed = new StringBuilder(s).reverse().toString(); + + assertThat(StringUtils.isPalindrome(s)).isEqualTo(s.equals(reversed)); + } + + @Property + void mirroredStringsArePalindromes(@ForAll @AlphaChars @NotEmpty @StringLength(max = 20) String s, + @ForAll @AlphaChars char middle) { + var reversed = new StringBuilder(s).reverse().toString(); + + assertThat(StringUtils.isPalindrome(s + reversed)).isTrue(); + assertThat(StringUtils.isPalindrome(s + middle + reversed)).isTrue(); + } + + @Property + void aShuffleIsAnAnagram(@ForAll @AlphaChars String s, @ForAll long seed) { + var chars = s.chars().mapToObj(c -> (char) c).collect(Collectors.toCollection(ArrayList::new)); + Collections.shuffle(chars, new Random(seed)); + var shuffled = chars.stream().map(String::valueOf).collect(Collectors.joining()); + + assertThat(StringUtils.isAnagram(s, shuffled)).isTrue(); + assertThat(StringUtils.isAnagram(s.toUpperCase(), shuffled.toLowerCase())).isTrue(); + } + + @Property + void isAnagramAgreesWithLetterCounts(@ForAll @AlphaChars @StringLength(max = 6) String a, + @ForAll @AlphaChars @StringLength(max = 6) String b) { + assertThat(StringUtils.isAnagram(a, b)).isEqualTo(letterCounts(a).equals(letterCounts(b))); + } + + private static java.util.Map letterCounts(String s) { + return s.toLowerCase().chars().boxed().collect(Collectors.groupingBy(c -> c, Collectors.counting())); + } +}