Skip to content

Commit

Permalink
Add nextCircularListIndex to KiwiLists (#1240)
Browse files Browse the repository at this point in the history
This method handles the simple calculation of getting the next index of
a circular list (or array), wrapping around to the beginning when at the
end. It also provides input validation on the list size and index.

I am adding this because, rather unbelievably, I was not able to find
something like it in any "standard" utility libraries (Google Guava,
Apache Commons Lang or Collections), etc. Maybe one exists, but I could
not find it. The closest I found was the CircularFifoQueue in Apache
Commons Collections.
  • Loading branch information
sleberknight authored Jan 11, 2025
1 parent d1a2871 commit 61c043a
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 1 deletion.
15 changes: 15 additions & 0 deletions src/main/java/org/kiwiproject/collect/KiwiLists.java
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,21 @@ public static <T> List<T> newListStartingAtCircularOffset(List<T> input, long st
return IntStream.range(0, size).mapToObj(i -> input.get((int) (startOffset + i) % size)).toList();
}

/**
* Returns the next index in a circular list. When the current index is at the end of the list, it
* wraps around to the beginning of the list.
*
* @param currentIndex the current index of a circular list
* @param listSize the size of the list
* @return the index following the current index, which may wrap around to zero
*/
public static int nextCircularListIndex(int currentIndex, int listSize) {
checkArgument(listSize > 0, "listSize must be positive");
checkArgument(currentIndex > -1 && currentIndex < listSize,
"currentIndex must be in the range [0, %s]", (listSize - 1));
return (currentIndex + 1) % listSize;
}

/**
* Returns a view of the portion of the given list excluding the first element.
* <p>
Expand Down
107 changes: 106 additions & 1 deletion src/test/java/org/kiwiproject/collect/KiwiListsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.kiwiproject.junit.jupiter.ClearBoxTest;
Expand All @@ -25,6 +26,7 @@
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.random.RandomGenerator;
import java.util.stream.IntStream;
import java.util.stream.Stream;

Expand Down Expand Up @@ -409,12 +411,115 @@ void shouldReturnNewListAtCorrectStartingOffset() {
}

@Test
void shouldWrapWhenOffsetIsBeyonfEndOfList() {
void shouldWrapWhenOffsetIsBeyondEndOfList() {
assertThat(KiwiLists.newListStartingAtCircularOffset(newArrayList("zero", "one", "two", "three"), 4))
.containsExactly("zero", "one", "two", "three");
}
}

@Nested
class NextCircularListIndex {

@ParameterizedTest
@ValueSource(ints = { -100, -25, -5, -1, 0 })
void shouldRequirePositiveListSize(int listSize) {
assertThatIllegalArgumentException()
.isThrownBy(() -> KiwiLists.nextCircularListIndex(0, listSize))
.withMessage("listSize must be positive");
}

@ParameterizedTest
@CsvSource(textBlock = """
1, 1
2, 1
2, 2
3, 2
4, 3
4, 4
10, 9
10, 10
20, 20
50, 49
100, 99
100, 100
-1, 3
-4, 5
-5, 3
""")
void shouldRequireValidCurrentIndex(int currentIndex, int listSize) {
assertThatIllegalArgumentException()
.isThrownBy(() -> KiwiLists.nextCircularListIndex(currentIndex, listSize))
.withMessage("currentIndex must be in the range [0, %d]", (listSize - 1));
}

@RepeatedTest(10)
void shouldAlwaysReturnZero_ForListSizeOfOne() {
assertThat(KiwiLists.nextCircularListIndex(0, 1)).isZero();
}

@ParameterizedTest
@CsvSource(textBlock = """
0, 2, 1
1, 2, 0
0, 3, 1
1, 3, 2
2, 3, 0
0, 5, 1
1, 5, 2
2, 5, 3
3, 5, 4
4, 5, 0
98, 100, 99
99, 100, 0
""")
void shouldReturnExpectedNextIndex(int currentIndex, int listSize, int expectedNextIndex) {
assertThat(KiwiLists.nextCircularListIndex(currentIndex, listSize))
.isEqualTo(expectedNextIndex);
}

@RepeatedTest(10)
void shouldCycleThroughIndices_AndReturnToBeginningOfList() {
var random = RandomGenerator.getDefault();
var listSize = random.nextInt(3, 26);
var numLoops = random.nextInt(1, 5);
var numIterations = listSize * numLoops;
var maxIndex = listSize - 1;
var currentIndex = 0;

for (int i = 0; i < numIterations; i++) {
currentIndex = getAndAssertNextCircularIndex(currentIndex, listSize, maxIndex);
}

assertThat(currentIndex)
.describedAs("After looping %d times through %d elements, we should be back at index 0",
numIterations, listSize)
.isZero();
}

private static int getAndAssertNextCircularIndex(int currentIndex, int listSize, int maxIndex) {
var nextIndex = KiwiLists.nextCircularListIndex(currentIndex, listSize);

if (currentIndex == maxIndex) {
assertThat(nextIndex)
.describedAs("If we're at index %d of an %d element list, the next index should be 0",
currentIndex, listSize)
.isZero();
} else {
var expectedNextIndex = currentIndex + 1;
assertThat(nextIndex)
.describedAs("If we're at index %d of an %d element list, the next index should be %d",
currentIndex, listSize, expectedNextIndex)
.isEqualTo(expectedNextIndex);
}

return nextIndex;
}
}

@Nested
class SubListExcludingFirst {

Expand Down

0 comments on commit 61c043a

Please sign in to comment.