cmd : commands) {
+ Objects.requireNonNull(cmd, "command entry must not be null");
+ if (cmd.size() != 3) {
+ throw new IllegalArgumentException("Command must contain exactly 3 integers: [type, tokenId, T]");
+ }
+
+ int type = cmd.get(CMD_TYPE_INDEX);
+ int tokenId = cmd.get(CMD_TOKEN_ID_INDEX);
+ int timestamp = cmd.get(CMD_TIME_INDEX);
+
+ if (type == ACTION_CREATE) {
+ tokenExpiries.put(tokenId, timestamp + expiryLimit);
+ } else if (type == ACTION_RESET) {
+ Integer currentExpiry = tokenExpiries.get(tokenId);
+ if (currentExpiry != null && currentExpiry >= timestamp) {
+ tokenExpiries.put(tokenId, timestamp + expiryLimit);
+ }
+ }
+ }
+
+ int finalTimestamp = commands.getLast().get(CMD_TIME_INDEX);
+
+ return (int) tokenExpiries.values().stream()
+ .filter(expiry -> expiry > finalTimestamp)
+ .count();
+ }
+}
diff --git a/src/main/java/com/shortthirdman/primekit/hackerearth/UpgradeEnhancementOptimizer.java b/src/main/java/com/shortthirdman/primekit/hackerearth/UpgradeEnhancementOptimizer.java
new file mode 100644
index 0000000..a54904d
--- /dev/null
+++ b/src/main/java/com/shortthirdman/primekit/hackerearth/UpgradeEnhancementOptimizer.java
@@ -0,0 +1,85 @@
+package com.shortthirdman.primekit.hackerearth;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Solves the optimal upgrade selection problem under a fixed currency constraint.
+ *
+ * Each available upgrade index {@code i} incurs a given cost and yields an
+ * enhancement gain equal to {@code 2^i}[cite: 1]. Because every power of two is strictly greater
+ * than the sum of all preceding powers of two (i.e., {@code \sum_{j=0}^{i-1} 2^j = 2^i - 1 < 2^i}),
+ * the problem exhibits the greedy-choice property. This optimizer selects upgrades
+ * greedily starting from the highest available power down to zero.
+ *
+ *
Thread Safety: This class is stateless, immutable, and thread-safe.
+ *
+ * @since 1.2.1
+ * @author ShortThirdMan
+ */
+public class UpgradeEnhancementOptimizer {
+
+ private static final int MOD = 1_000_000_007;
+
+ /**
+ * Calculates the maximum enhancement obtainable without exceeding the provided budget.
+ *
+ *
Enhancement values grow exponentially such that the item at index {@code i}
+ * contributes {@code 2^i} enhancement[cite: 1]. The returned sum is reduced modulo {@code 10^9 + 7}[cite: 1].
+ *
+ *
Algorithm Details
+ *
+ * - Precomputes powers of two modulo {@code 1,000,000,007}.
+ * - Greedy Selection: Evaluates items in descending index order
+ * ({@code n - 1} down to {@code 0}), deducting the item cost whenever sufficient
+ * budget remains.
+ *
+ *
+ * Complexity
+ *
+ * - Time Complexity: {@code O(n)}, where {@code n} is the size of {@code upgradeCosts}.
+ * - Space Complexity: {@code O(n)} auxiliary memory for modular exponent precomputations.
+ *
+ *
+ * @param upgradeCosts non-null list of non-negative integers representing upgrade costs,
+ * where element at index {@code i} yields enhancement {@code 2^i}[cite: 1]
+ * @param budget maximum spendable currency, must be non-negative
+ * @return the maximum enhancement score achievable within budget, modulo {@code 10^9 + 7}[cite: 1]
+ * @throws NullPointerException if {@code upgradeCosts} is {@code null}
+ * @throws IllegalArgumentException if {@code budget} is negative
+ */
+ public int optimizeInAppUpgrades(List upgradeCosts, int budget) {
+ Objects.requireNonNull(upgradeCosts, "upgradeCosts must not be null");
+ if (budget < 0) {
+ throw new IllegalArgumentException("budget cannot be negative: " + budget);
+ }
+
+ int n = upgradeCosts.size();
+ if (n == 0) {
+ return 0;
+ }
+
+ // Precompute powers of 2: 2^0, 2^1, ..., 2^(n-1) modulo 10^9 + 7
+ int[] pow2 = new int[n];
+ pow2[0] = 1;
+ for (int i = 1; i < n; i++) {
+ pow2[i] = (pow2[i - 1] * 2) % MOD;
+ }
+
+ long remainingBudget = budget;
+ int totalEnhancement = 0;
+
+ for (int i = n - 1; i >= 0; i--) {
+ int cost = upgradeCosts.get(i);
+ if (remainingBudget >= cost) {
+ remainingBudget -= cost;
+ totalEnhancement += pow2[i];
+ if (totalEnhancement >= MOD) {
+ totalEnhancement -= MOD;
+ }
+ }
+ }
+
+ return totalEnhancement;
+ }
+}
diff --git a/src/main/java/com/shortthirdman/primekit/leetcode/WordLadders.java b/src/main/java/com/shortthirdman/primekit/leetcode/WordLadders.java
index eed9495..a852d02 100644
--- a/src/main/java/com/shortthirdman/primekit/leetcode/WordLadders.java
+++ b/src/main/java/com/shortthirdman/primekit/leetcode/WordLadders.java
@@ -18,7 +18,7 @@ public class WordLadders {
* @param beginWord the word to begin with
* @param endWord the word to end with
* @param wordList the list of words
- * @return
+ * @return the list of ladders
*/
public List> findLadders(String beginWord, String endWord, List wordList) {
List> result = new ArrayList<>();
@@ -33,7 +33,7 @@ public List> findLadders(String beginWord, String endWord, List minLen) {
return result;
}
@@ -61,7 +61,7 @@ public List> findLadders(String beginWord, String endWord, List mockResponse = (HttpResponse) mock(HttpResponse.class);
when(mockResponse.body()).thenReturn(json);
- when(mockClient.send(
- any(HttpRequest.class),
- ArgumentMatchers.>any()
- )).thenReturn(mockResponse);
- // Intercept static factory `HttpClient.newHttpClient()`
+ // Fix: Use doReturn to avoid Java compiler generic mismatch on send()
+ doReturn(mockResponse).when(mockClient).send(any(HttpRequest.class), any());
+
try (MockedStatic mockedStatic = mockStatic(HttpClient.class)) {
mockedStatic.when(HttpClient::newHttpClient).thenReturn(mockClient);
@@ -53,8 +44,7 @@ void testFetchPosts_UsingMockito_NoHttpCall() throws Exception {
assertEquals(2, posts.size());
mockedStatic.verify(HttpClient::newHttpClient);
- verify(mockClient, times(1))
- .send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class));
+ verify(mockClient, times(1)).send(any(HttpRequest.class), any());
}
}
diff --git a/src/test/java/com/shortthirdman/primekit/hackerearth/TokenExpiryTrackerTest.java b/src/test/java/com/shortthirdman/primekit/hackerearth/TokenExpiryTrackerTest.java
new file mode 100644
index 0000000..c046d77
--- /dev/null
+++ b/src/test/java/com/shortthirdman/primekit/hackerearth/TokenExpiryTrackerTest.java
@@ -0,0 +1,267 @@
+package com.shortthirdman.primekit.hackerearth;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("TokenExpiryTracker Tests")
+class TokenExpiryTrackerTest {
+
+ TokenExpiryTracker app;
+
+ @BeforeEach
+ void setUp() {
+ app = new TokenExpiryTracker();
+ }
+
+ @Nested
+ @DisplayName("Happy Path & Standard Scenarios")
+ class StandardExecutionTests {
+
+ @Test
+ @DisplayName("Should return correct active count for standard problem example")
+ void testProblemExampleScenario() {
+ int expiryLimit = 4;
+ List> commands = List.of(
+ List.of(0, 1, 1), // Create 1 at T=1 -> exp: 5
+ List.of(0, 2, 2), // Create 2 at T=2 -> exp: 6
+ List.of(1, 1, 5), // Reset 1 at T=5 -> exp: 9
+ List.of(1, 2, 7) // Reset 2 at T=7 -> ignored (expired at 6)
+ );
+
+ int result = app.numberOfTokens(expiryLimit, commands);
+
+ assertEquals(1, result, "Only token 1 should remain active at T=7 (expires at 9 > 7)");
+ }
+
+ @Test
+ @DisplayName("Should keep all tokens active when expiry exceeds maximum timestamp")
+ void testAllTokensRemainActive() {
+ int expiryLimit = 10;
+ List> commands = List.of(
+ List.of(0, 101, 1), // exp: 11
+ List.of(0, 102, 2), // exp: 12
+ List.of(0, 103, 3) // exp: 13
+ );
+
+ int result = app.numberOfTokens(expiryLimit, commands);
+
+ assertEquals(3, result, "All tokens expire after T=3 and must be active");
+ }
+
+ @Test
+ @DisplayName("Should report zero active tokens when all tokens expire before or at final timestamp")
+ void testAllTokensExpired() {
+ int expiryLimit = 2;
+ List> commands = List.of(
+ List.of(0, 1, 1), // exp: 3
+ List.of(0, 2, 2), // exp: 4
+ List.of(1, 999, 10) // ignored reset on non-existent token at T=10
+ );
+
+ int result = app.numberOfTokens(expiryLimit, commands);
+
+ assertEquals(0, result, "Tokens 1 and 2 expired prior to final timestamp T=10");
+ }
+
+ @Test
+ @DisplayName("Should support multiple resets extending token lifespan repeatedly")
+ void testMultipleSuccessfulResetsOnSameToken() {
+ int expiryLimit = 5;
+ List> commands = List.of(
+ List.of(0, 42, 1), // exp: 6
+ List.of(1, 42, 4), // reset -> exp: 9
+ List.of(1, 42, 8), // reset -> exp: 13
+ List.of(1, 42, 12) // reset -> exp: 17
+ );
+
+ int result = app.numberOfTokens(expiryLimit, commands);
+
+ assertEquals(1, result, "Token 42 should remain active up to T=17");
+ }
+ }
+
+ @Nested
+ @DisplayName("Boundary & Edge Cases")
+ class EdgeCaseTests {
+
+ @Test
+ @DisplayName("Should return 0 when commands list is empty")
+ void testEmptyCommandsList() {
+ int result = app.numberOfTokens(5, Collections.emptyList());
+
+ assertEquals(0, result, "Empty commands list must produce 0 active tokens");
+ }
+
+ @Test
+ @DisplayName("Should keep token active for a single create command")
+ void testSingleCreateCommand() {
+ List> commands = List.of(List.of(0, 1, 10));
+
+ int result = app.numberOfTokens(3, commands);
+
+ assertEquals(1, result, "Token created at T=10 with expiry 13 is active at T=10");
+ }
+
+ @Test
+ @DisplayName("Should consider token expired when expiration exactly equals final timestamp")
+ void testTokenExpiresExactlyAtFinalTimestamp() {
+ int expiryLimit = 5;
+ List> commands = List.of(
+ List.of(0, 1, 1), // exp: 6
+ List.of(1, 99, 6) // non-existent token reset at T=6
+ );
+
+ int result = app.numberOfTokens(expiryLimit, commands);
+
+ assertEquals(0, result, "Token expires at 6, which is not strictly greater than max time 6");
+ }
+
+ @Test
+ @DisplayName("Should allow token reset exactly on its expiration timestamp")
+ void testTokenResetExactlyOnBoundaryTimestamp() {
+ int expiryLimit = 4;
+ List> commands = List.of(
+ List.of(0, 1, 2), // exp: 6
+ List.of(1, 1, 6) // reset exactly at T=6 -> exp: 10
+ );
+
+ int result = app.numberOfTokens(expiryLimit, commands);
+
+ assertEquals(1, result, "Reset on expiration timestamp is valid, new expiry becomes 10 > 6");
+ }
+
+ @Test
+ @DisplayName("Should overwrite token expiration when recreation occurs with same token ID")
+ void testTokenRecreationOverwritesPriorState() {
+ int expiryLimit = 3;
+ List> commands = List.of(
+ List.of(0, 1, 1), // exp: 4
+ List.of(0, 1, 10) // re-created at T=10 -> exp: 13
+ );
+
+ int result = app.numberOfTokens(expiryLimit, commands);
+
+ assertEquals(1, result, "Re-creating token 1 updates its lifetime to 13 > 10");
+ }
+
+ @Test
+ @DisplayName("Should handle multiple commands occurring at the exact same timestamp")
+ void testConcurrentTimestamps() {
+ int expiryLimit = 3;
+ List> commands = List.of(
+ List.of(0, 1, 5), // exp: 8
+ List.of(0, 2, 5), // exp: 8
+ List.of(1, 1, 5) // reset at T=5 -> exp: 8
+ );
+
+ int result = app.numberOfTokens(expiryLimit, commands);
+
+ assertEquals(2, result, "Both tokens active at final timestamp T=5 with exp=8");
+ }
+
+ @Test
+ @DisplayName("Should ignore reset commands for tokens that never existed")
+ void testResetNonExistentTokenIgnored() {
+ int expiryLimit = 5;
+ List> commands = List.of(
+ List.of(1, 555, 2),
+ List.of(1, 666, 4)
+ );
+
+ int result = app.numberOfTokens(expiryLimit, commands);
+
+ assertEquals(0, result, "Non-existent tokens must not be created or counted");
+ }
+
+ @Test
+ @DisplayName("Should ignore reset attempt strictly after expiration has passed")
+ void testResetAfterExpirationIgnored() {
+ int expiryLimit = 2;
+ List> commands = List.of(
+ List.of(0, 1, 1), // exp: 3
+ List.of(1, 1, 4) // ignored: T=4 is strictly greater than current exp=3
+ );
+
+ int result = app.numberOfTokens(expiryLimit, commands);
+
+ assertEquals(0, result, "Expired token cannot be renewed and remains inactive");
+ }
+ }
+
+ @Nested
+ @DisplayName("Negative Scenarios & Exception Handling")
+ class ExceptionTests {
+
+ @Test
+ @DisplayName("Should throw NullPointerException when commands list is null")
+ void testNullCommandsListThrowsException() {
+ assertThrows(NullPointerException.class, () ->
+ app.numberOfTokens(5, null)
+ );
+ }
+
+ @Test
+ @DisplayName("Should throw NullPointerException when a command row is null")
+ void testNullCommandEntryThrowsException() {
+ List> commands = new ArrayList<>();
+ commands.add(List.of(0, 1, 1));
+ commands.add(null);
+
+ assertThrows(NullPointerException.class, () ->
+ app.numberOfTokens(5, commands)
+ );
+ }
+
+ @Test
+ @DisplayName("Should throw IllegalArgumentException when expiryLimit is zero")
+ void testZeroExpiryLimitThrowsException() {
+ List> commands = List.of(List.of(0, 1, 1));
+
+ assertThrows(IllegalArgumentException.class, () ->
+ app.numberOfTokens(0, commands)
+ );
+ }
+
+ @Test
+ @DisplayName("Should throw IllegalArgumentException when expiryLimit is negative")
+ void testNegativeExpiryLimitThrowsException() {
+ List> commands = List.of(List.of(0, 1, 1));
+
+ assertThrows(IllegalArgumentException.class, () ->
+ app.numberOfTokens(-5, commands)
+ );
+ }
+
+ @Test
+ @DisplayName("Should throw IllegalArgumentException when command tuple has fewer than 3 elements")
+ void testCommandTupleUnderflowThrowsException() {
+ List> commands = List.of(
+ List.of(0, 1) // Missing timestamp
+ );
+
+ assertThrows(IllegalArgumentException.class, () ->
+ app.numberOfTokens(5, commands)
+ );
+ }
+
+ @Test
+ @DisplayName("Should throw IllegalArgumentException when command tuple has more than 3 elements")
+ void testCommandTupleOverflowThrowsException() {
+ List> commands = List.of(
+ List.of(0, 1, 2, 999) // Extra element
+ );
+
+ assertThrows(IllegalArgumentException.class, () ->
+ app.numberOfTokens(5, commands)
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/shortthirdman/primekit/hackerearth/UpgradeEnhancementOptimizerTest.java b/src/test/java/com/shortthirdman/primekit/hackerearth/UpgradeEnhancementOptimizerTest.java
new file mode 100644
index 0000000..db81172
--- /dev/null
+++ b/src/test/java/com/shortthirdman/primekit/hackerearth/UpgradeEnhancementOptimizerTest.java
@@ -0,0 +1,205 @@
+package com.shortthirdman.primekit.hackerearth;
+
+import org.junit.jupiter.api.*;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("UpgradeEnhancementOptimizer Tests")
+class UpgradeEnhancementOptimizerTest {
+
+ UpgradeEnhancementOptimizer app;
+
+ @BeforeEach
+ void setUp() {
+ app = new UpgradeEnhancementOptimizer();
+ }
+
+ @AfterEach
+ void tearDown() {
+ app = null;
+ }
+
+ @Nested
+ @DisplayName("Sample and Positive Functional Cases")
+ class PositiveScenarios {
+
+ @Test
+ @DisplayName("Should solve the HackerRank example case correctly")
+ void shouldSolveStandardExampleCase() {
+ List costs = List.of(10, 20, 14, 40, 50); // indices 0 to 4[cite: 1]
+ int budget = 70; //[cite: 1]
+
+ // Upgrade 4 (cost 50, val 16) + Upgrade 2 (cost 14, val 4) = 20[cite: 1]
+ int result = app.optimizeInAppUpgrades(costs, budget);
+
+ assertEquals(20, result); //[cite: 1]
+ }
+
+ @Test
+ @DisplayName("Should solve Sample Case 0 where all upgrades can be afforded")
+ void shouldSolveSampleCase0AffordingAllUpgrades() {
+ List costs = List.of(3, 4, 1); //[cite: 2]
+ int budget = 8; //[cite: 2]
+
+ // Total cost = 3 + 4 + 1 = 8 <= 8; total value = 2^0 + 2^1 + 2^2 = 1 + 2 + 4 = 7[cite: 2]
+ int result = app.optimizeInAppUpgrades(costs, budget);
+
+ assertEquals(7, result); //[cite: 2]
+ }
+
+ @Test
+ @DisplayName("Should select only the single highest upgrade if budget permits nothing else")
+ void shouldSelectSingleHighestUpgradeWhenRemainingBudgetIsInsufficient() {
+ List costs = List.of(15, 10, 100);
+ int budget = 105;
+
+ // Upgrade 2 (cost 100, val 4) leaves budget 5; cannot afford index 1 or 0
+ int result = app.optimizeInAppUpgrades(costs, budget);
+
+ assertEquals(4, result);
+ }
+
+ @Test
+ @DisplayName("Should greedily skip expensive higher upgrade when cost exceeds budget")
+ void shouldSkipExpensiveHigherUpgradeAndPickAffordableLowerUpgrades() {
+ List costs = List.of(5, 5, 200);
+ int budget = 10;
+
+ // Upgrade 2 (cost 200) skipped; Upgrade 1 (cost 5, val 2) + Upgrade 0 (cost 5, val 1) = 3
+ int result = app.optimizeInAppUpgrades(costs, budget);
+
+ assertEquals(3, result);
+ }
+
+ @Test
+ @DisplayName("Should return correct value when exact budget equals single item cost")
+ void shouldHandleSingleItemExactBudget() {
+ List costs = List.of(42);
+ int budget = 42;
+
+ // Upgrade 0 has value 2^0 = 1
+ int result = app.optimizeInAppUpgrades(costs, budget);
+
+ assertEquals(1, result);
+ }
+
+ @Test
+ @DisplayName("Should correctly wrap modulo (10^9 + 7) for large power sum results")
+ void shouldApplyModuloWhenTotalEnhancementExceedsModuloBoundary() {
+ // 31 upgrades all with cost 0 -> sum = 2^31 - 1 = 2,147,483,647
+ // Modulo 1,000,000,007 -> 2,147,483,647 % 1,000,000,007 = 147,483,633
+ List costs = Collections.nCopies(31, 0);
+ int budget = 0;
+
+ int result = app.optimizeInAppUpgrades(costs, budget);
+
+ assertEquals(147_483_633, result);
+ }
+ }
+
+ @Nested
+ @DisplayName("Edge Cases")
+ class EdgeCases {
+
+ @Test
+ @DisplayName("Should return 0 when upgrade costs list is empty")
+ void shouldReturnZeroWhenUpgradeCostsListIsEmpty() {
+ int result = app.optimizeInAppUpgrades(List.of(), 500);
+
+ assertEquals(0, result);
+ }
+
+ @Test
+ @DisplayName("Should return 0 when budget is 0 and all items cost greater than 0")
+ void shouldReturnZeroWhenBudgetIsZeroAndCostsPositive() {
+ List costs = List.of(10, 20, 30);
+ int budget = 0;
+
+ int result = app.optimizeInAppUpgrades(costs, budget);
+
+ assertEquals(0, result);
+ }
+
+ @Test
+ @DisplayName("Should return 0 when budget is strictly less than minimum item cost")
+ void shouldReturnZeroWhenBudgetIsLessThanAnyCost() {
+ List costs = List.of(50, 100, 200);
+ int budget = 25;
+
+ int result = app.optimizeInAppUpgrades(costs, budget);
+
+ assertEquals(0, result);
+ }
+
+ @Test
+ @DisplayName("Should handle 0 cost upgrades when budget is 0")
+ void shouldIncludeZeroCostUpgradesEvenWithZeroBudget() {
+ List costs = List.of(0, 10, 0); // index 0 and 2 are free
+ int budget = 0;
+
+ // index 2 (val 4) + index 0 (val 1) = 5
+ int result = app.optimizeInAppUpgrades(costs, budget);
+
+ assertEquals(5, result);
+ }
+
+ @Test
+ @DisplayName("Should handle max budget constraint up to 10^9 without integer overflow")
+ void shouldHandleMaximumAllowedBudget() {
+ List costs = List.of(100_000, 100_000); //[cite: 2]
+ int budget = 1_000_000_000; // 10^9[cite: 2]
+
+ // Both can be afforded: 2^0 + 2^1 = 3
+ int result = app.optimizeInAppUpgrades(costs, budget);
+
+ assertEquals(3, result);
+ }
+ }
+
+ @Nested
+ @DisplayName("Exception and Negative Validations")
+ class ExceptionCases {
+
+ @Test
+ @DisplayName("Should throw NullPointerException when upgradeCosts list is null")
+ void shouldThrowNullPointerExceptionWhenListIsNull() {
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> app.optimizeInAppUpgrades(null, 50)
+ );
+
+ assertEquals("upgradeCosts must not be null", exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should throw IllegalArgumentException when budget is negative")
+ void shouldThrowIllegalArgumentExceptionWhenBudgetIsNegative() {
+ List costs = List.of(10, 20);
+ int invalidBudget = -1;
+
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> app.optimizeInAppUpgrades(costs, invalidBudget)
+ );
+
+ assertEquals("budget cannot be negative: -1", exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should throw IllegalArgumentException with large negative budget")
+ void shouldThrowIllegalArgumentExceptionForLargeNegativeBudget() {
+ List costs = List.of(1, 2, 3);
+ int invalidBudget = Integer.MIN_VALUE;
+
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> app.optimizeInAppUpgrades(costs, invalidBudget)
+ );
+
+ assertEquals("budget cannot be negative: " + Integer.MIN_VALUE, exception.getMessage());
+ }
+ }
+}
\ No newline at end of file