Repository: TheAlgorithms/Java
Pull request #4209 (2023-05-31 15:41:27)

refactor: generalise `NthUglyNumber`
This PR:
- refactors the [`NthUglyNumber`](https://github.com/TheAlgorithms/Java/blob/4bbc4bd69f2a68963ba733df744c6ea356aba109/src/main/java/com/thealgorithms/maths/NthUglyNumber.java#L18),
- adds tests.

<!-- For completed items, change [ ] to [x] -->

- [x] I have read [CONTRIBUTING.md](https://github.com/TheAlgorithms/Java/blob/master/CONTRIBUTING.md).
- [x] This pull request is all my own work -- I have not plagiarized it.
- [x] All filenames are in PascalCase.
- [x] All functions and variable names follow Java naming conventions.
- [x] All new algorithms have a URL in their comments that points to Wikipedia or other similar explanations.


vil02: I am not sure about the memoization. Please note that:
- the old method [`getNthUglyNo`](https://github.com/TheAlgorithms/Java/blob/4bbc4bd69f2a68963ba733df744c6ea356aba109/src/main/java/com/thealgorithms/maths/NthUglyNumber.java#L21) is not `static`,
- the [`ugly` array](https://github.com/TheAlgorithms/Java/blob/4bbc4bd69f2a68963ba733df744c6ea356aba109/src/main/java/com/thealgorithms/maths/NthUglyNumber.java#L22) stores the same data as [uglyNumbers](https://github.com/TheAlgorithms/Java/blob/9d1290d5d5e00d2256e2a4eae80fb535278eb267/src/main/java/com/thealgorithms/maths/NthUglyNumber.java#L20).

The generic part came up as an accident: I wanted to get rig of the variables like `two`, `three`, `five`, `nm2`, `nm3` and `nm5`.

Patch:
diff --git a/src/main/java/com/thealgorithms/maths/NthUglyNumber.java b/src/main/java/com/thealgorithms/maths/NthUglyNumber.java
index 4c040f57..6daeb267 100644
--- a/src/main/java/com/thealgorithms/maths/NthUglyNumber.java
+++ b/src/main/java/com/thealgorithms/maths/NthUglyNumber.java
@@ -1,57 +1,82 @@
-// Ugly numbers are numbers whose only prime factors are 2, 3 or 5. The sequence 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, … shows the first 11 ugly numbers.
-// By convention, 1 is included.
-// A program to find the nth Ugly number
-// Algorithm :
-// Initialize three-pointers two, three, and five pointing to zero.
-// Take 3 variables nm2, nm3, and nm5 to keep track of next multiple of 2,3 and 5.
-// Make an array of size n to store the ugly numbers with 1 at 0th index.
-// Initialize a variable next which stores the value of the last element in the array.
-// Run a loop n-1 times and perform steps 6,7 and 8.
-// Update the values of nm2, nm3, nm5 as ugly[two]*2, ugly[three]*3, ugly[5]*5 respectively.
-// Select the minimum value from nm2, nm3, and nm5 and increment the pointer related to it.
-// Store the minimum value in variable next and array.
-// Return next.
 package com.thealgorithms.maths;
 
-import java.util.*;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.lang.IllegalArgumentException;
 
-class NthUglyNumber {
 
-    /* Function to get the nth ugly number*/
-    public long getNthUglyNo(int n) {
-        long[] ugly = new long[n];
-        int two = 0, three = 0, five = 0;
-        long nm2 = 2, nm3 = 3, nm5 = 5;
-        long next = 1;
+/**
+ * @brief class computing the n-th ugly number (when they are sorted)
+ * @details the ugly numbers with base [2, 3, 5] are all numbers of the form 2^a*3^b^5^c,
+ *   where the exponents a, b, c are non-negative integers.
+ *   Some properties of ugly numbers:
+ *     - base [2, 3, 5] ugly numbers are the 5-smooth numbers, cf. https://oeis.org/A051037
+ *     - base [2, 3, 5, 7] ugly numbers are 7-smooth numbers, cf. https://oeis.org/A002473
+ *     - base [2] ugly numbers are the non-negative powers of 2,
+ *     - the base [2, 3, 5] ugly numbers are the same as base [5, 6, 2, 3, 5] ugly numbers
+ */
+public class NthUglyNumber {
+    ArrayList<Long> uglyNumbers = new ArrayList<>(Arrays.asList(1L));
+    final int[] baseNumbers;
+    HashMap<Integer, Integer> positions = new HashMap<>();
 
-        ugly[0] = 1;
+    /**
+     * @brief initialized the object allowing to compute ugly numbers with given base
+     * @param baseNumbers the given base of ugly numbers
+     * @exception IllegalArgumentException baseNumber is empty
+     */
+    NthUglyNumber(int[] baseNumbers) {
+        if (baseNumbers.length == 0) {
+            throw new IllegalArgumentException("baseNumbers must be non-empty.");
+        }
 
-        for (int i = 1; i < n; i++) {
-            next = Math.min(nm2, Math.min(nm3, nm5));
+        this.baseNumbers = baseNumbers;
+        for (final var baseNumber : baseNumbers) {
+            this.positions.put(baseNumber, 0);
+        }
+    }
 
-            ugly[i] = next;
-            if (next == nm2) {
-                two = two + 1;
-                nm2 = ugly[two] * 2;
-            }
-            if (next == nm3) {
-                three = three + 1;
-                nm3 = ugly[three] * 3;
-            }
-            if (next == nm5) {
-                five = five + 1;
-                nm5 = ugly[five] * 5;
+    /**
+     * @param n the zero-based-index of the queried ugly number
+     * @exception IllegalArgumentException n is negative
+     * @return the n-th ugly number (starting from index 0)
+     */
+    public Long get(int n) {
+        if (n < 0) {
+            throw new IllegalArgumentException("n must be non-negative.");
+        }
+
+        while (uglyNumbers.size() <= n) {
+            addUglyNumber();
+        }
+
+        return uglyNumbers.get(n);
+    }
+
+    private void addUglyNumber() {
+        uglyNumbers.add(computeMinimalCandidate());
+        updatePositions();
+    }
+
+    private void updatePositions() {
+        final var lastUglyNumber = uglyNumbers.get(uglyNumbers.size() - 1);
+        for (final var baseNumber : baseNumbers) {
+            if (computeCandidate(baseNumber) == lastUglyNumber) {
+                positions.put(baseNumber, positions.get(baseNumber) + 1);
             }
         }
-        return next;
     }
 
-    public static void main(String[] args) {
-        Scanner sc = new Scanner(System.in);
-        System.out.println("Enter the value of n : ");
-        int n = sc.nextInt();
-        NthUglyNumber ob = new NthUglyNumber();
-        long ugly = ob.getNthUglyNo(n);
-        System.out.println("nth Ugly number is : " + ugly);
+    private long computeCandidate(int candidateBase) {
+        return candidateBase * uglyNumbers.get(positions.get(candidateBase));
+    }
+
+    private long computeMinimalCandidate() {
+        long res = Long.MAX_VALUE;
+        for (final var baseNumber : baseNumbers) {
+            res = Math.min(res, computeCandidate(baseNumber));
+        }
+        return res;
     }
 }


Test patch:
diff --git a/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java b/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java
new file mode 100644
index 00000000..597d0892
--- /dev/null
+++ b/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java
@@ -0,0 +1,87 @@
+package com.thealgorithms.maths;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.HashMap;
+
+import org.junit.jupiter.api.Test;
+
+public class NthUglyNumberTest {
+    @Test
+    public void testGetWithNewObject() {
+        HashMap<Integer, Long> testCases = new HashMap<>();
+        testCases.put(0, 1L);
+        testCases.put(1, 2L);
+        testCases.put(2, 3L);
+        testCases.put(3, 4L);
+        testCases.put(4, 5L);
+        testCases.put(5, 6L);
+        testCases.put(9, 12L);
+        testCases.put(19, 36L);
+        testCases.put(52, 270L);
+        testCases.put(1078, 84934656L);
+        testCases.put(1963, 6973568802L);
+
+        for (final var tc : testCases.entrySet()) {
+            var uglyNumbers = new NthUglyNumber(new int[] {2, 3, 5});
+            assertEquals(uglyNumbers.get(tc.getKey()), tc.getValue());
+
+            var otherUglyNumbers = new NthUglyNumber(new int[] {5, 25, 6, 2, 3, 5});
+            assertEquals(otherUglyNumbers.get(tc.getKey()), tc.getValue());
+        }
+    }
+
+    @Test
+    public void testGetWithSameObject() {
+        HashMap<Integer, Long> testCases = new HashMap<>();
+        testCases.put(0, 1L);
+        testCases.put(1, 2L);
+        testCases.put(2, 3L);
+        testCases.put(3, 4L);
+        testCases.put(4, 5L);
+        testCases.put(5, 6L);
+        testCases.put(6, 7L);
+        testCases.put(1499, 1984500L);
+        testCases.put(1572, 2449440L);
+        testCases.put(1658, 3072000L);
+        testCases.put(6625, 4300800000L);
+
+        var uglyNumbers = new NthUglyNumber(new int[] {7, 2, 5, 3});
+        for (final var tc : testCases.entrySet()) {
+            assertEquals(uglyNumbers.get(tc.getKey()), tc.getValue());
+        }
+        
+        assertEquals(uglyNumbers.get(999), 385875);
+    }
+
+    @Test
+    public void testGetWithBase1() {
+        var uglyNumbers = new NthUglyNumber(new int[] {1});
+        assertEquals(uglyNumbers.get(10), 1);
+    }
+
+    @Test
+    public void testGetWithBase2() {
+        var uglyNumbers = new NthUglyNumber(new int[] {2});
+        assertEquals(uglyNumbers.get(5), 32);
+    }
+
+
+    @Test
+    public void testGetThrowsAnErrorForNegativeInput() {
+        var uglyNumbers = new NthUglyNumber(new int[] {1, 2});
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> uglyNumbers.get(-1)
+        );
+    }
+
+    @Test
+    public void testConstructorThrowsAnErrorForEmptyInput() {
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> new NthUglyNumber(new int[] {})
+        );
+    }
+}