Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/main/java/com/cosimo/utilities/timed/Cooldown.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

import java.util.concurrent.TimeUnit;

/**
* Mutable implementation where the cooldown can be extended by a duration.
*/
public class Cooldown implements ICooldown {

/**
Expand All @@ -17,7 +20,6 @@ public class Cooldown implements ICooldown {
*
* @param duration For how long this cooldown will last
* @param unit {@link TimeUnit} of the given cooldown duration parameter
* @see Cooldown - duration unit conversion to milliseconds
*/
public Cooldown(long duration, @NonNull TimeUnit unit) {
this.end = this.getCurrentTime() + this.toThisTime(duration, unit);
Expand Down
15 changes: 7 additions & 8 deletions src/main/java/com/cosimo/utilities/timed/Cooldowns.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import java.util.Map;

/**
* A {@link Map} of {@link ICooldown} updated lazily, on access, with the {@link #cleanup()} method for purging expired
* entries when needed.
* A {@link Map} of {@link ICooldown} cleared lazily with the {@link #clearExpired()} method for purging expired entries
* when needed.
*
* @param <K> Key type of this class's {@link Map}, {@link String} is suggested as it provides many variations for
* unique keys
* @param <V> Expected {@link ICooldown} implementation
*/
@SuppressWarnings("unused")
public class Cooldowns<K, V extends ICooldown> extends HashMap<K, V> {
public Cooldowns(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
Expand All @@ -26,18 +27,16 @@ public Cooldowns() {
super();
}

public Cooldowns(@NonNull Map<? extends K, ? extends V> map) {
public Cooldowns(@NonNull Map<K, ? extends V> map) {
super(map);
}

/**
* Removes all {@link ICooldown} that have expired.
*
* @return This instance, useful for chaining
* @return Whether any {@link ICooldown} has expired and was removed
*/
@NonNull
public Cooldowns<K, V> cleanup() {
this.entrySet().removeIf(entry -> entry.getValue().isExpired());
return this;
public boolean clearExpired() {
return this.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
}
32 changes: 27 additions & 5 deletions src/main/java/com/cosimo/utilities/timed/ICooldown.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,43 @@ default boolean isExpired() {
}

/**
* Returns how much time is left until the end of this cooldown, expressed in a given {@link TimeUnit}.
* Returns how much time is left until the end of this cooldown, expressed in a given {@link TimeUnit}, minimally
* zero.
*
* @param unit {@link TimeUnit} in which the remaining milliseconds should be converted to
* @return The remaining cooldown time, expressed as a double in the given {@link TimeUnit}
* @return The remaining cooldown time that can't be negative, expressed as a long in the given {@link TimeUnit}
*/
default long getRemaining(@NonNull TimeUnit unit) {
return this.fromThisTime(this.getRemaining(), unit);
return Math.max(0, this.getDifference(unit));
}

/**
* Returns how much time is left until the end of this cooldown, even negative if it has already expired.
* Returns the timestamp difference between current one and the end of this cooldown, minimally zero, which would
* indicate it has already expired.
*
* @return Remaining time of any sign
* @return Remaining time that can't be negative
*/
default long getRemaining() {
return Math.max(0, this.getDifference());
}

/**
* Returns the timestamp difference between current one and the end of this cooldown, expressed in a given
* {@link TimeUnit}.
*
* @param unit {@link TimeUnit} in which the remaining milliseconds should be converted to
* @return The remaining cooldown time, expressed as a long in the given {@link TimeUnit}
*/
default long getDifference(@NonNull TimeUnit unit) {
return this.fromThisTime(this.getDifference(), unit);
}

/**
* Returns how much time is left until the end of this cooldown, even negative if it has already expired.
*
* @return Remaining time of any mathematical sign
*/
default long getDifference() {
return this.getExpiration() - this.getCurrentTime();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

import java.util.concurrent.TimeUnit;

/**
* Immutable implementation for tracking cooldown expiration only.
*/
@SuppressWarnings("unused")
public class ImmutableCooldown implements ICooldown {

/**
Expand Down
38 changes: 30 additions & 8 deletions src/test/java/com/cosimo/utilities/timed/CooldownTests.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package com.cosimo.utilities.timed;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

import java.util.Map;
import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.*;

@Execution(ExecutionMode.CONCURRENT)
public class CooldownTests {

/**
* Permitted time difference from setting a time value to testing it.
*/
public static final int TOLERANCE = 50;
public static final int MILLISECOND_TOLERANCE = 50;

@Test
public void testToThisTime() {
Expand All @@ -33,26 +36,43 @@ public void testFromThisTime() {
@Test
public void testGetCurrentTime() {
final TimeStandard timeStandard = new TimeStandard() {
@Override
public long getCurrentTime() {
return System.nanoTime();
}
};
final long currentTime = System.currentTimeMillis();
final long currentTime = System.nanoTime();
final long diffInMs = Math.abs(timeStandard.getCurrentTime() - currentTime) / 1000;

assertTrue(Math.abs(timeStandard.getCurrentTime() - currentTime) < TOLERANCE);
assertTrue(diffInMs < MILLISECOND_TOLERANCE);
}

@Test
public void testIsExpired() throws InterruptedException {
final ICooldown cooldown = new Cooldown(50);

assertFalse(cooldown.isExpired());

Thread.sleep(60);

assertTrue(cooldown.isExpired());

assertEquals(0, cooldown.getRemaining());
assertEquals(0, cooldown.getRemaining(TimeUnit.SECONDS));

assertTrue(cooldown.getDifference() < 0);
assertTrue(cooldown.getDifference(TimeUnit.NANOSECONDS) < 0);
}

@Test
public void testGetRemaining() {
public void testGetRemainingAndDifference() {
final ICooldown cooldown = new Cooldown(5_000);
final long difference = cooldown.getDifference();
final long remaining = cooldown.getRemaining();

assertTrue(remaining <= 5_000 && remaining > 0);
assertTrue(difference <= 5_000 && difference > 0);
assertEquals(difference, remaining);
assertTrue(cooldown.getDifference(TimeUnit.SECONDS) <= 5);
assertTrue(cooldown.getRemaining(TimeUnit.SECONDS) <= 5);
}

Expand Down Expand Up @@ -93,18 +113,20 @@ public void testCooldownsCleanup() throws InterruptedException {
cooldowns.put("test1", new Cooldown(50));
cooldowns.put("test2", new Cooldown(5_000));

Thread.sleep(50 + TOLERANCE);
Thread.sleep(50 + MILLISECOND_TOLERANCE);

cooldowns.cleanup();
assertTrue(cooldowns.clearExpired());

assertFalse(cooldowns.containsKey("test1"));
assertTrue(cooldowns.containsKey("test2"));

assertFalse(cooldowns.clearExpired());
}

@Test
public void testCooldownsConstructorWithMap() {
final var initialMap = Map.of("key1", new Cooldown(5000));
final Cooldowns<String, ICooldown> cooldowns = new Cooldowns<>(initialMap);
final var cooldowns = new Cooldowns<>(initialMap);

assertTrue(cooldowns.containsKey("key1"));
}
Expand Down
Loading