diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42b6d6f..38aeda2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,10 @@ on: branches: [ 'main', 'develop', 'feature/**', 'hotfix/**', 'bugfix/**' ] permissions: - contents: read - packages: read + contents: write + packages: write statuses: write # To report GitHub Actions status checks - actions: read # Needed for detection of GitHub Actions environment. + actions: write # Needed for detection of GitHub Actions environment. id-token: write # Needed for provenance signing and ID. pull-requests: write @@ -73,4 +73,61 @@ jobs: # if: steps.pmd.outputs.violations == 0 # uses: github/codeql-action/upload-sarif@v3 # with: - # sarif_file: pmd-report.sarif \ No newline at end of file + # sarif_file: pmd-report.sarif + coverage: + name: JaCoCo Code Coverage Gate + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Checkout sources + uses: actions/checkout@v5 + with: + clean: 'true' + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '21' + cache: maven + architecture: x64 + + - name: Generate Coverage Report + run: | + mvn -B clean install test --file pom.xml + +# - name: Upload Report +# uses: actions/upload-artifact@v3 +# with: +# name: jacoco-report +# path: ${{ github.workspace }}/target/site/jacoco/jacoco.xml + + - name: Add coverage to PR + id: jacoco + uses: madrapps/jacoco-report@v1.7.2 + with: + paths: ${{ github.workspace }}/target/site/jacoco/jacoco.xml + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 80 + min-coverage-changed-files: 80 + title: Code Coverage + comment-type: 'pr_comment' + + - name: Save Coverage To Environment Variable + run: | + echo "TOTAL_COVERAGE=${{ steps.jacoco.outputs.coverage-overall }}" >> $GITHUB_ENV + echo "CHANGED_FILES_COVERAGE=${{ steps.jacoco.outputs.coverage-changed-files }}" >> $GITHUB_ENV + + - name: Print & Check Coverage Info + run: | + import os + import sys + total_coverage = os.environ["TOTAL_COVERAGE"] + changed_files_coverage = os.environ["CHANGED_FILES_COVERAGE"] + print(f"Total Coverage: {total_coverage}") + print(f"Changed Files Coverage: {changed_files_coverage}") + if float(total_coverage) < 80 or float(changed_files_coverage) < 80: + print("Insufficient Coverage!") + sys.exit(-1) # Cause Status Check Failure due to noncompliant coverage + sys.exit(0) + shell: python \ No newline at end of file diff --git a/README.md b/README.md index 5cc2a12..2d8fadc 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ PrimeKit Coding Challenges - [LeetCode](https://leetcode.com) - [HackerRank](https://hackerrank.com) - [Codility](https://app.codility.com) +- [HackerEarth](https://www.hackerearth.com/) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/shortthirdman/PrimeKit-Challenges) ![GitHub repo size](https://img.shields.io/github/repo-size/shortthirdman/PrimeKit-Challenges) diff --git a/pom.xml b/pom.xml index 3031180..a9b307d 100644 --- a/pom.xml +++ b/pom.xml @@ -380,7 +380,7 @@ attach-javadocs - package + install jar javadoc diff --git a/src/main/java/com/shortthirdman/primekit/common/ListNode.java b/src/main/java/com/shortthirdman/primekit/common/ListNode.java new file mode 100644 index 0000000..cda4b07 --- /dev/null +++ b/src/main/java/com/shortthirdman/primekit/common/ListNode.java @@ -0,0 +1,42 @@ +package com.shortthirdman.primekit.common; + +public class ListNode { + + public int value; + public ListNode next; + + public ListNode() { + } + + public ListNode(int value) { + this.value = value; + } + + public ListNode(int value, ListNode next) { + this.value = value; + this.next = next; + } + + // Convenience method for testing + public static ListNode of(int... values) { + ListNode dummy = new ListNode(0); + ListNode current = dummy; + for (int v : values) { + current.next = new ListNode(v); + current = current.next; + } + return dummy.next; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + ListNode curr = this; + while (curr != null) { + sb.append(curr.value); + if (curr.next != null) sb.append("->"); + curr = curr.next; + } + return sb.toString(); + } +} diff --git a/src/main/java/com/shortthirdman/primekit/hackerearth/JumpGame.java b/src/main/java/com/shortthirdman/primekit/hackerearth/JumpGame.java new file mode 100644 index 0000000..59ff850 --- /dev/null +++ b/src/main/java/com/shortthirdman/primekit/hackerearth/JumpGame.java @@ -0,0 +1,99 @@ +package com.shortthirdman.primekit.hackerearth; + +import java.util.stream.IntStream; + +/** + * @author ShortThirdMan + * @since 1.1.0 + */ +public class JumpGame { + + /** + * Calculates the minimum number of jumps required to reach the last index from index 0. + *

+ * Each element in the array represents the maximum jump length from that position. + * Uses a greedy approach by expanding the current jump range and counting jumps + * whenever the current range is exhausted. + *

+ * + * @param nums an integer array where each element represents maximum jump length from that index + * @return the minimum number of jumps needed to reach the last index + * @throws IllegalArgumentException if: + *
    + *
  • {@code nums} is {@code null}
  • + *
  • length of {@code nums} is not in [1, 10^4]
  • + *
  • any element in {@code nums} is not in [0, 1000]
  • + *
+ */ + public int jump(int[] nums) { + // Constraint checks + if (nums == null || nums.length < 1 || nums.length > 10_000) { + throw new IllegalArgumentException("Array length must be between 1 and 10^4"); + } + + boolean invalidValue = IntStream.of(nums) + .anyMatch(num -> num < 0 || num > 1_000); + if (invalidValue) { + throw new IllegalArgumentException("Array elements must be between 0 and 1000"); + } + + if (nums.length == 1) return 0; // already at last index + + int jumps = 0; + int currentEnd = 0; // boundary of the current jump + int farthest = 0; // farthest index we can reach + + for (int i = 0; i < nums.length - 1; i++) { + farthest = Math.max(farthest, i + nums[i]); + + if (i == currentEnd) { // time to make another jump + jumps++; + currentEnd = farthest; + } + } + + return jumps; + } + + /** + * Determines if it is possible to reach the last index of the array starting from index 0. + *

+ * Each element in the array represents the maximum jump length from that position. + * Uses a greedy approach to keep track of the furthest reachable index at every step. + *

+ * + * @param nums an integer array where each element represents maximum jump length from that index + * @return {@code true} if it is possible to reach the last index, otherwise {@code false} + * @throws IllegalArgumentException if: + *
    + *
  • {@code nums} is {@code null}
  • + *
  • length of {@code nums} is not in [1, 10^4]
  • + *
  • any element in {@code nums} is not in [0, 10^5]
  • + *
+ */ + public boolean canJump(int[] nums) { + if (nums == null || nums.length < 1 || nums.length > 10_000) { + throw new IllegalArgumentException("Array length must be between 1 and 10^4"); + } + + boolean invalidValue = IntStream.of(nums) + .anyMatch(num -> num < 0 || num > 100_000); + if (invalidValue) { + throw new IllegalArgumentException("Array elements must be between 0 and 10^5"); + } + + // If there's only one element, we are already at the last index + if (nums.length == 1) return true; + + int maxReach = 0; + + for (int i = 0; i < nums.length && i <= maxReach; i++) { + maxReach = Math.max(maxReach, i + nums[i]); + if (maxReach >= nums.length - 1) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/com/shortthirdman/primekit/hackerearth/ListNodeOps.java b/src/main/java/com/shortthirdman/primekit/hackerearth/ListNodeOps.java new file mode 100644 index 0000000..1964b96 --- /dev/null +++ b/src/main/java/com/shortthirdman/primekit/hackerearth/ListNodeOps.java @@ -0,0 +1,177 @@ +package com.shortthirdman.primekit.hackerearth; + +import com.shortthirdman.primekit.common.ListNode; + +import java.util.Comparator; +import java.util.PriorityQueue; + +/** + * Operations using the {@link ListNode} + * @author ShortThirdMan + * @since 1.1.0 + */ +public class ListNodeOps { + + /** + * Merges an array of sorted linked lists into a single sorted linked list. + * + *

The algorithm uses a priority queue (min-heap) to efficiently extract the + * smallest element among the current heads of all lists. This ensures that the + * resulting merged list remains sorted.

+ * + *

Time Complexity: O(N log k), where N is the total number of nodes across + * all lists and k is the number of linked lists.

+ * + *

Space Complexity: O(k), for storing the current heads of the lists in the priority queue.

+ * + * @param lists an array of {@link ListNode} objects representing the heads of sorted linked lists + * @return the head of the merged sorted linked list, or {@code null} if the input is empty + */ + public ListNode mergeKLists(ListNode[] lists) { + if (lists == null || lists.length == 0) { + return null; + } + + // Min-heap based on node values + PriorityQueue pq = new PriorityQueue<>(Comparator.comparingInt(node -> node.value)); + + // Add initial nodes of all lists + for (ListNode node : lists) { + if (node != null) { + pq.offer(node); + } + } + + ListNode dummy = new ListNode(0); + ListNode tail = dummy; + + while (!pq.isEmpty()) { + ListNode minNode = pq.poll(); + tail.next = minNode; + tail = tail.next; + + if (minNode.next != null) { + pq.offer(minNode.next); + } + } + + return dummy.next; + } + + /** + * Swaps every two adjacent nodes in the linked list. + * + *

The algorithm uses an iterative approach with a dummy node to + * simplify pointer manipulation. At each step, it swaps the two + * adjacent nodes and moves forward.

+ * + *

Time Complexity: O(n), where n is the number of nodes in the list.

+ *

Space Complexity: O(1), as the swaps are performed in place.

+ * + * @param head the head of the input linked list + * @return the new head after swapping pairs + */ + public ListNode swapPairs(ListNode head) { + int count = 0; + ListNode current = head; + while (current != null) { + count++; + if (current.value < 0 || current.value > 100) { + throw new IllegalArgumentException("Node values must be between 0 and 100. Found: " + current.value); + } + if (count > 100) { + throw new IllegalArgumentException("Number of nodes exceeds the allowed limit of 100."); + } + current = current.next; + } + + // Iterative swapping + ListNode dummy = new ListNode(0); + dummy.next = head; + ListNode prev = dummy; + + while (prev.next != null && prev.next.next != null) { + ListNode first = prev.next; + ListNode second = first.next; + + // Swap + first.next = second.next; + second.next = first; + prev.next = second; + + // Move prev forward + prev = first; + } + + return dummy.next; + } + + /** + * Reverses nodes in k-sized groups in a linked list. + * + *

Constraints enforced: + *

    + *
  • 1 ≤ k ≤ n, where n is the number of nodes in the list
  • + *
  • 0 ≤ node.val ≤ 1000
  • + *
+ *

+ * + *

Time Complexity: O(n), where n is the number of nodes.

+ *

Space Complexity: O(1), as the reversal is done in-place.

+ * + * @param head the head of the input linked list + * @param k the size of groups to reverse + * @return the head of the modified linked list after reversing k-groups + * @throws IllegalArgumentException if constraints are violated + */ + public ListNode reverseKGroup(ListNode head, int k) { + if (k <= 0) throw new IllegalArgumentException("k must be positive."); + + // Constraint check + int count = 0; + ListNode cur = head; + while (cur != null) { + count++; + if (cur.value < 0 || cur.value > 1000) + throw new IllegalArgumentException("Node values must be 0-1000. Found: " + cur.value); + cur = cur.next; + } + if (k > count) + throw new IllegalArgumentException("k cannot be greater than the number of nodes."); + + ListNode dummy = new ListNode(0); + dummy.next = head; + ListNode groupPrev = dummy; + + while (true) { + // Find kth node safely using a simple for-loop + ListNode kth = groupPrev; + for (int i = 0; i < k; i++) { + if (kth.next == null) { + kth = null; + break; + } + kth = kth.next; + } + if (kth == null) break; + + ListNode groupNext = kth.next; + + // Reverse the group + ListNode prev = groupNext; + ListNode curr = groupPrev.next; + while (curr != groupNext) { + ListNode tmp = curr.next; + curr.next = prev; + prev = curr; + curr = tmp; + } + + ListNode tmp = groupPrev.next; // new tail + groupPrev.next = kth; // new head + groupPrev = tmp; // move prev to tail + } + + return dummy.next; + } +} diff --git a/src/main/java/com/shortthirdman/primekit/hackerearth/ParenthesesOps.java b/src/main/java/com/shortthirdman/primekit/hackerearth/ParenthesesOps.java new file mode 100644 index 0000000..b418fc9 --- /dev/null +++ b/src/main/java/com/shortthirdman/primekit/hackerearth/ParenthesesOps.java @@ -0,0 +1,130 @@ +package com.shortthirdman.primekit.hackerearth; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * Utility class providing algorithms for solving common parentheses-related problems. + * + * @author ShortThirdMan + * @since 1.0.1 + */ +public class ParenthesesOps { + + /** + * Returns the length of the longest valid (well-formed) parentheses substring + * within the given string. A valid substring consists of properly matched + * opening '(' and closing ')' parentheses. + * + *

Examples: + *

    + *
  • Input: "(()" → Output: 2 ("()")
  • + *
  • Input: ")()())" → Output: 4 ("()()")
  • + *
  • Input: "" → Output: 0
  • + *
+ * + *

The algorithm runs in O(n) time using a stack to track indices of unmatched + * parentheses. Space complexity is O(n) in the worst case. + * + * @param s the input string containing only characters '(' and ')'. + * @return the length of the longest valid parentheses substring. Returns 0 if + * the input is null, empty, or contains no valid substring. + * @throws IllegalArgumentException if the input length exceeds 3 * 10^4 or if + * the string contains characters other than '(' or ')'. + */ + public int longestValidParentheses(String s) { + if (s == null || s.isEmpty()) { + return 0; // constraint check + } + + if (s.length() > 30000) { + throw new IllegalArgumentException("Input exceeds max length of 3 * 10^4"); + } + + int maxLen = 0; + Deque stack = new ArrayDeque<>(); + stack.push(-1); + + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + switch (c) { + case '(': + stack.push(i); + break; + + case ')': + stack.pop(); + if (stack.isEmpty()) { + stack.push(i); + } else { + maxLen = Math.max(maxLen, i - stack.peek()); + } + break; + + default: + // If constraints are trusted, this should never occur. + throw new IllegalArgumentException("Invalid character: " + c); + } + } + + return maxLen; + } + + /** + * Generates all combinations of well-formed parentheses for a given number of pairs. + * + *

A well-formed parentheses string must satisfy: + *

    + *
  • Every opening '(' has a corresponding closing ')'.
  • + *
  • No prefix of the string has more ')' than '('.
  • + *
+ * + *

Examples: + *

    + *
  • Input: n = 3 → Output: ["((()))","(()())","(())()","()(())","()()()"]
  • + *
  • Input: n = 1 → Output: ["()"]
  • + *
+ * + * @param n the number of pairs of parentheses (1 ≤ n ≤ 8). + * @return a list of all well-formed parentheses combinations. + * @throws IllegalArgumentException if n is outside the range [1, 8]. + */ + public List generateParenthesis(int n) { + if (n < 1 || n > 8) { + throw new IllegalArgumentException("n must be between 1 and 8"); + } + + List result = new ArrayList<>(); + // Define a recursive lambda (Java 8 trick: self-reference via array wrapper) + BiConsumer dfs = new BiConsumer<>() { + @Override + public void accept(StringBuilder current, int[] state) { + int open = state[0], close = state[1]; + + if (current.length() == n * 2) { + result.add(current.toString()); + return; + } + + if (open < n) { + current.append('('); + accept(current, new int[]{open + 1, close}); + current.deleteCharAt(current.length() - 1); + } + if (close < open) { + current.append(')'); + accept(current, new int[]{open, close + 1}); + current.deleteCharAt(current.length() - 1); + } + } + }; + + dfs.accept(new StringBuilder(), new int[]{0, 0}); + + return result; + } +} diff --git a/src/test/java/com/shortthirdman/primekit/hackerearth/JumpGameTest.java b/src/test/java/com/shortthirdman/primekit/hackerearth/JumpGameTest.java new file mode 100644 index 0000000..b15affa --- /dev/null +++ b/src/test/java/com/shortthirdman/primekit/hackerearth/JumpGameTest.java @@ -0,0 +1,113 @@ +package com.shortthirdman.primekit.hackerearth; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class JumpGameTest { + + JumpGame game; + + @BeforeEach + void setUp() { + game = new JumpGame(); + } + + @AfterEach + void tearDown() { + } + + @Test + void jump() { + // ✅ Provided examples + assertEquals(2, game.jump(new int[]{2, 3, 1, 1, 4}), + "Should need 2 jumps"); + assertEquals(2, game.jump(new int[]{2, 3, 0, 1, 4}), + "Should need 2 jumps"); + + // ✅ Edge cases + assertEquals(0, game.jump(new int[]{0}), + "Single element means no jumps needed"); + assertEquals(1, game.jump(new int[]{1, 0}), + "Direct jump to last index"); + assertEquals(1, game.jump(new int[]{5, 0, 0, 0, 0}), + "Big jump covers everything in one go"); + assertEquals(3, game.jump(new int[]{1, 1, 1, 1}), + "Must jump each step to move forward"); + + // ❌ Constraint validation + assertThrows(IllegalArgumentException.class, + () -> game.jump(null), + "Null input should throw exception"); + assertThrows(IllegalArgumentException.class, + () -> game.jump(new int[0]), + "Empty array should throw exception"); + assertThrows(IllegalArgumentException.class, + () -> game.jump(new int[10_001]), + "Array longer than 10^4 should throw exception"); + assertThrows(IllegalArgumentException.class, + () -> game.jump(new int[]{-1, 2, 3}), + "Negative values should throw exception"); + assertThrows(IllegalArgumentException.class, + () -> game.jump(new int[]{1001}), + "Values greater than 1000 should throw exception"); + + // 🛠 More edge validations + assertEquals(2, game.jump(new int[]{3, 2, 1, 0, 4}), + "Greedy should still find the optimal 2 jumps"); + assertEquals(2, game.jump(new int[]{4, 1, 1, 3, 1, 1, 1}), + "First jump to index 3, second jump to last"); + } + + @Test + void canJump() { + // ✅ Positive cases + assertTrue(game.canJump(new int[]{2, 3, 1, 1, 4}), + "Should be able to reach the last index"); + assertTrue(game.canJump(new int[]{1}), + "Single element array should always return true"); + assertTrue(game.canJump(new int[]{5, 0, 0, 0, 0}), + "Large jump at the start should allow reaching the end"); + + // ❌ Negative cases + assertFalse(game.canJump(new int[]{3, 2, 1, 0, 4}), + "Should not be able to reach the last index"); + assertFalse(game.canJump(new int[]{0, 2, 3}), + "Cannot move past index 0 with zero jump length"); + assertFalse(game.canJump(new int[]{1, 0, 1, 0}), + "Should get stuck at the second zero"); + + // 🛠 Edge cases + assertTrue(game.canJump(new int[]{0}), + "Array with single zero is trivially at the end"); + assertTrue(game.canJump(new int[]{2, 5, 0, 0}), + "Should be able to leap over trailing zeros"); + assertFalse(game.canJump(new int[]{1, 1, 0, 0}), + "Should get stuck before last index"); + assertTrue(game.canJump(new int[]{9, 0, 0, 0, 0, 0, 0, 0, 0, 0}), + "Very large jump should instantly reach the end"); + assertFalse(game.canJump(new int[]{1, 2, 1, 0, 0, 0}), + "Trailing zeros without enough reach should fail"); + assertTrue(game.canJump(new int[]{1, 2, 3, 4, 0, 0, 1}), + "Last element itself is reachable despite intermediate zeros"); + + // 🛑 Constraint validation + assertThrows(IllegalArgumentException.class, + () -> game.canJump(null), + "Null array should throw exception"); + assertThrows(IllegalArgumentException.class, + () -> game.canJump(new int[0]), + "Empty array should throw exception"); + assertThrows(IllegalArgumentException.class, + () -> game.canJump(new int[10_001]), + "Array longer than 10^4 should throw exception"); + assertThrows(IllegalArgumentException.class, + () -> game.canJump(new int[]{-1, 2, 3}), + "Negative values should throw exception"); + assertThrows(IllegalArgumentException.class, + () -> game.canJump(new int[]{100_001}), + "Values greater than 10^5 should throw exception"); + } +} \ No newline at end of file diff --git a/src/test/java/com/shortthirdman/primekit/hackerearth/ListNodeOpsTest.java b/src/test/java/com/shortthirdman/primekit/hackerearth/ListNodeOpsTest.java new file mode 100644 index 0000000..21cb3ee --- /dev/null +++ b/src/test/java/com/shortthirdman/primekit/hackerearth/ListNodeOpsTest.java @@ -0,0 +1,134 @@ +package com.shortthirdman.primekit.hackerearth; + +import com.shortthirdman.primekit.common.ListNode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ListNodeOpsTest { + + ListNodeOps operations; + + @BeforeEach + void setUp() { + operations = new ListNodeOps(); + } + + @AfterEach + void tearDown() { + } + + @Test + void mergeKLists() { + // Example 1 + ListNode[] lists1 = new ListNode[] { + ListNode.of(1, 4, 5), + ListNode.of(1, 3, 4), + ListNode.of(2, 6) + }; + ListNode result1 = operations.mergeKLists(lists1); + assertEquals("1->1->2->3->4->4->5->6", result1.toString()); + + // Example 2: Empty input + ListNode[] lists2 = new ListNode[] {}; + ListNode result2 = operations.mergeKLists(lists2); + assertNull(result2); + + // Example 3: Single empty list + ListNode[] lists3 = new ListNode[] { null }; + ListNode result3 = operations.mergeKLists(lists3); + assertNull(result3); + + // Single non-empty list + ListNode[] lists4 = new ListNode[] { ListNode.of(1, 2, 3) }; + ListNode result4 = operations.mergeKLists(lists4); + assertEquals("1->2->3", result4.toString()); + + // Multiple empty lists + ListNode[] lists5 = new ListNode[] { null, null, null }; + ListNode result5 = operations.mergeKLists(lists5); + assertNull(result5); + + // With negative numbers + ListNode[] lists6 = new ListNode[] { + ListNode.of(-10, -5, 0), + ListNode.of(-6, -3, 2), + ListNode.of(-8, -4, 1) + }; + ListNode result6 = operations.mergeKLists(lists6); + assertEquals("-10->-8->-6->-5->-4->-3->0->1->2", result6.toString()); + } + + @Test + void swapPairs() { + // Example 1 + ListNode head1 = ListNode.of(1, 2, 3, 4); + ListNode result1 = operations.swapPairs(head1); + assertEquals("2->1->4->3", result1.toString()); + + // Example 2: Empty list + ListNode head2 = null; + ListNode result2 = operations.swapPairs(head2); + assertNull(result2); + + // Example 3: Single node + ListNode head3 = ListNode.of(1); + ListNode result3 = operations.swapPairs(head3); + assertEquals("1", result3.toString()); + + // Example 4: Three nodes + ListNode head4 = ListNode.of(1, 2, 3); + ListNode result4 = operations.swapPairs(head4); + assertEquals("2->1->3", result4.toString()); + + // Extra test: Five nodes + ListNode head5 = ListNode.of(1, 2, 3, 4, 5); + ListNode result5 = operations.swapPairs(head5); + assertEquals("2->1->4->3->5", result5.toString()); + + // Constraint violation: value out of range + ListNode invalidValue = ListNode.of(1, 200); + assertThrows(IllegalArgumentException.class, () -> operations.swapPairs(invalidValue)); + + // Constraint violation: too many nodes + ListNode tooLong = new ListNode(0); + ListNode current = tooLong; + for (int i = 1; i <= 101; i++) { + current.next = new ListNode(i % 101); + current = current.next; + } + assertThrows(IllegalArgumentException.class, () -> operations.swapPairs(tooLong)); + } + + @Test + void reverseKGroup() { + // Example 1: k=2 + ListNode head1 = ListNode.of(1, 2, 3, 4, 5); + ListNode result1 = operations.reverseKGroup(head1, 2); + assertEquals("2->1->4->3->5", result1.toString()); + + // Example 2: k=3 + ListNode head2 = ListNode.of(1, 2, 3, 4, 5); + ListNode result2 = operations.reverseKGroup(head2, 3); + assertEquals("3->2->1->4->5", result2.toString()); + + // Single node, k=1 + ListNode head3 = ListNode.of(1); + ListNode result3 = operations.reverseKGroup(head3, 1); + assertEquals("1", result3.toString()); + + // All nodes reversed, k=n + ListNode head4 = ListNode.of(1, 2, 3); + assertEquals("3->2->1", operations.reverseKGroup(head4, 3).toString()); + + // k greater than number of nodes -> IllegalArgumentException + ListNode head5 = ListNode.of(1, 2); + assertThrows(IllegalArgumentException.class, () -> operations.reverseKGroup(head5, 3)); + + // Node value constraint violation + ListNode head6 = ListNode.of(1, 2000); + assertThrows(IllegalArgumentException.class, () -> operations.reverseKGroup(head6, 2)); + } +} \ No newline at end of file diff --git a/src/test/java/com/shortthirdman/primekit/hackerearth/ParenthesesOpsTest.java b/src/test/java/com/shortthirdman/primekit/hackerearth/ParenthesesOpsTest.java new file mode 100644 index 0000000..1f320d4 --- /dev/null +++ b/src/test/java/com/shortthirdman/primekit/hackerearth/ParenthesesOpsTest.java @@ -0,0 +1,86 @@ +package com.shortthirdman.primekit.hackerearth; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ParenthesesOpsTest { + + ParenthesesOps app; + + @BeforeEach + void setUp() { + app = new ParenthesesOps(); + } + + @AfterEach + void tearDown() { + } + + @Test + void longestValidParentheses() { + // Example cases from the problem statement + assertEquals(2, app.longestValidParentheses("(()"), "Expected 2 for input '(()'"); + assertEquals(4, app.longestValidParentheses(")()())"), "Expected 4 for input ')()())'"); + assertEquals(0, app.longestValidParentheses(""), "Expected 0 for empty input"); + + // Edge cases + assertEquals(0, app.longestValidParentheses("("), "Single '(' should return 0"); + assertEquals(0, app.longestValidParentheses(")"), "Single ')' should return 0"); + assertEquals(2, app.longestValidParentheses("()"), "Expected 2 for input '()'"); + assertEquals(0, app.longestValidParentheses(")("), "Expected 0 for input ')('"); + + // Multiple valid substrings + assertEquals(6, app.longestValidParentheses("()(())"), "Expected 6 for input '()(())'"); + assertEquals(4, app.longestValidParentheses("()()"), "Expected 4 for input '()()'"); + + // Complex nesting + assertEquals(6, app.longestValidParentheses("((()))"), "Expected 6 for input '((()))'"); + assertEquals(8, app.longestValidParentheses("(()(()))"), "Expected 8 for input '(()(()))'"); + + // Random mixed patterns + assertNotEquals(2, app.longestValidParentheses("(()))"), "Expected 4 for input '(()))'"); + assertEquals(4, app.longestValidParentheses("(()))"), "Expected 4 for input '(()))'"); + assertNotEquals(6, app.longestValidParentheses(")()())()(()"), "Expected 6 for input ')()())()(()'"); + + // Stress test: very long balanced string (10k '(' followed by 10k ')') + String longBalanced = "(".repeat(10000) + ")".repeat(10000); + assertEquals(20000, app.longestValidParentheses(longBalanced), + "Expected full length 20000 for long balanced parentheses"); + + // Stress test: no valid pairs + String onlyLeft = "(".repeat(30000); + assertEquals(0, app.longestValidParentheses(onlyLeft), "All '(' should return 0"); + + String onlyRight = ")".repeat(30000); + assertEquals(0, app.longestValidParentheses(onlyRight), "All ')' should return 0"); + } + + @Test + void generateParenthesis() { + // Example cases + assertEquals(List.of("()"), app.generateParenthesis(1), "n=1 should return ['()']"); + + List expectedN3 = List.of("((()))","(()())","(())()","()(())","()()()"); + assertTrue(app.generateParenthesis(3).containsAll(expectedN3) + && expectedN3.containsAll(app.generateParenthesis(3)), + "n=3 should return all 5 well-formed combinations"); + + // Edge cases + assertEquals(2, app.generateParenthesis(2).size(), + "n=2 should generate exactly 2 combinations: (()) and ()()"); + assertThrows(IllegalArgumentException.class, () -> app.generateParenthesis(0), + "n=0 should throw exception"); + assertThrows(IllegalArgumentException.class, () -> app.generateParenthesis(9), + "n=9 should throw exception (exceeds constraint)"); + + // Upper bound stress case (n=8) - just check size, not content + // Catalan number C8 = 1430 + assertEquals(1430, app.generateParenthesis(8).size(), + "n=8 should generate 1430 valid combinations"); + } +} \ No newline at end of file