diff --git a/minecord-bot/src/main/java/com/tisawesomeness/minecord/mc/item/Container.java b/minecord-bot/src/main/java/com/tisawesomeness/minecord/mc/item/Container.java
new file mode 100644
index 00000000..b33ce227
--- /dev/null
+++ b/minecord-bot/src/main/java/com/tisawesomeness/minecord/mc/item/Container.java
@@ -0,0 +1,19 @@
+package com.tisawesomeness.minecord.mc.item;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * A container that can hold one or more stacks of Minecraft items.
+ */
+@Getter
+@RequiredArgsConstructor
+public enum Container {
+ STACK(1),
+ CHEST(27),
+ DOUBLE_CHEST(2 * 27),
+ CHEST_SHULKER(27 * 27),
+ DOUBLE_CHEST_SHULKER(2 * 27 * 27);
+
+ private final int slots;
+}
diff --git a/minecord-bot/src/main/java/com/tisawesomeness/minecord/mc/item/ItemCount.java b/minecord-bot/src/main/java/com/tisawesomeness/minecord/mc/item/ItemCount.java
new file mode 100644
index 00000000..19e612f3
--- /dev/null
+++ b/minecord-bot/src/main/java/com/tisawesomeness/minecord/mc/item/ItemCount.java
@@ -0,0 +1,102 @@
+package com.tisawesomeness.minecord.mc.item;
+
+import lombok.Getter;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A specific number of Minecraft items. Item count may be negative.
+ */
+public class ItemCount {
+
+ private final long itemCount;
+ @Getter private final int stackSize;
+
+ /**
+ * Creates a new item count.
+ * @param itemCount number of starting items
+ * @param stackSize stack size of the item
+ * @throws IllegalArgumentException if the stack size is not within 1-64
+ */
+ public ItemCount(long itemCount, int stackSize) {
+ this.itemCount = itemCount;
+ if (stackSize < 1 || 64 < stackSize) {
+ throw new IllegalArgumentException("stackSize must be between 1 and 64 but was " + stackSize);
+ }
+ this.stackSize = stackSize;
+
+ }
+
+ /**
+ * Creates a new item count with the added items and same stack size.
+ * @param items number of items to add
+ * @return new item count
+ */
+ public ItemCount addItems(long items) {
+ return new ItemCount(itemCount + items, stackSize);
+ }
+ /**
+ * Creates a new item count with the added stacks and same stack size.
+ * @param stacks number of stacks to add
+ * @return new item count
+ */
+ public ItemCount addStacks(long stacks) {
+ return addItems(stackSize * stacks);
+ }
+ /**
+ * Creates a new item count, adding the given number of containers, keeping the same stack size.
+ * @param container container holding the items
+ * @param count number of containers to add
+ * @return new item count
+ */
+ public ItemCount addContainers(Container container, long count) {
+ return addStacks(container.getSlots() * count);
+ }
+
+ /**
+ * @return item count
+ */
+ public long getCount() {
+ return itemCount;
+ }
+
+ /**
+ * Gets the exact number of containers needed to hold this item count.
+ * @param container container holding the items
+ * @return number of containers, possibly fractional
+ */
+ public double getExact(Container container) {
+ return (double) itemCount / (stackSize * container.getSlots());
+ }
+
+ /**
+ * Computes a combination of containers that holds this item count.
+ * Containers with higher capacities are prioritized.
+ *
Example: 1863 items is equal to 1 chest, 2 stacks, 7 items.
+ * @param containers containers allowed to be used
+ * @return list of length {@code containers.size() + 1}, each element is the amount of containers necessary in
+ * descending order, and one extra element at the end for leftover items (above example would return
+ * {@code [1, 2, 7]})
+ */
+ public List distribute(Collection containers) {
+ long stacks = itemCount / stackSize;
+ long items = itemCount % stackSize;
+
+ List list = new ArrayList<>();
+ for (int i = Container.values().length - 1; i >= 0; i--) {
+ Container container = Container.values()[i];
+ if (containers.contains(container)) {
+
+ list.add(stacks / container.getSlots());
+ stacks %= container.getSlots();
+
+ }
+ }
+
+ list.add(items);
+ return list;
+ }
+
+}
diff --git a/minecord-bot/src/test/java/com/tisawesomeness/minecord/mc/item/ItemCountTest.java b/minecord-bot/src/test/java/com/tisawesomeness/minecord/mc/item/ItemCountTest.java
new file mode 100644
index 00000000..cf742ef0
--- /dev/null
+++ b/minecord-bot/src/test/java/com/tisawesomeness/minecord/mc/item/ItemCountTest.java
@@ -0,0 +1,116 @@
+package com.tisawesomeness.minecord.mc.item;
+
+import com.tisawesomeness.minecord.util.Mth;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.*;
+
+public class ItemCountTest {
+
+ @ParameterizedTest
+ @ValueSource(longs = {0, 1, 5, 65})
+ public void testNew(long count) {
+ assertThat(new ItemCount(count, 64).getCount())
+ .isEqualTo(count);
+ }
+ @ParameterizedTest
+ @ValueSource(ints = {0, 65})
+ public void testNewInvalid(int stackSize) {
+ assertThatThrownBy(() -> new ItemCount(0, stackSize))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @ParameterizedTest
+ @ValueSource(longs = {-1, 0, 1, 65})
+ public void testAddItems(long count) {
+ assertThat(new ItemCount(5, 64).addItems(count).getCount())
+ .isEqualTo(5 + count);
+ }
+ @ParameterizedTest
+ @CsvSource({
+ "0, 64, 1, 64",
+ "2, 1, 5, 7",
+ "37, 16, 5, 117",
+ "15, 64, -2, -113"
+ })
+ public void testAddStacks(long initialCount, int stackSize, long stacks, long expectedCount) {
+ assertThat(new ItemCount(initialCount, stackSize).addStacks(stacks).getCount())
+ .isEqualTo(expectedCount);
+ }
+ @ParameterizedTest
+ @CsvSource({
+ "0, 64, STACK, 1, 64",
+ "0, 64, CHEST, -2, -3456",
+ "7, 16, DOUBLE_CHEST, 5, 4327",
+ "-165, 64, CHEST_SHULKER, 1, 46491",
+ "9164, 64, DOUBLE_CHEST_SHULKER, 3, 289100"
+ })
+ public void testAddContainers(long initialCount, int stackSize, Container container, long count, long expectedCount) {
+ assertThat(new ItemCount(initialCount, stackSize).addContainers(container, count).getCount())
+ .isEqualTo(expectedCount);
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "0, 64, STACK, 0.0",
+ "32, 64, STACK, 0.5",
+ "63, 64, STACK, 0.984375",
+ "64, 64, STACK, 1.0",
+ "128, 64, STACK, 2.0",
+ "-63, 64, STACK, -0.984375",
+ "2, 16, STACK, 0.125",
+ "83, 1, STACK, 83.0",
+ "27000, 64, CHEST, 15.625",
+ "27000, 64, DOUBLE_CHEST, 7.8125",
+ "139968, 64, CHEST_SHULKER, 3.0",
+ "93312, 64, DOUBLE_CHEST_SHULKER, 1.0"
+ })
+ public void testExact(long count, int stackSize, Container container, double expected) {
+ assertThat(new ItemCount(count, stackSize).getExact(container))
+ .isCloseTo(expected, within(Mth.EPSILON));
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "0, 64, 0, 0",
+ "2, 64, 0, 2",
+ "2, 1, 2, 0",
+ "69, 64, 1, 5",
+ "1863, 64, 29, 7",
+ "-1863, 64, -29, -7"
+ })
+ public void testDistributeStack(long count, int stackSize, long stacks, long items) {
+ assertThat(new ItemCount(count, stackSize).distribute(EnumSet.of(Container.STACK)))
+ .containsExactly(stacks, items);
+ }
+ @ParameterizedTest
+ @CsvSource({
+ "0, 64, 0, 0, 0",
+ "27, 1, 1, 0, 0",
+ "1863, 64, 1, 2, 7",
+ "-1863, 64, -1, -2, -7"
+ })
+ public void testDistributeChest(long count, int stackSize, long chests, long stacks, long items) {
+ assertThat(new ItemCount(count, stackSize).distribute(EnumSet.of(Container.STACK, Container.CHEST)))
+ .containsExactly(chests, stacks, items);
+ }
+ @ParameterizedTest
+ @CsvSource({
+ "0, 64, 0, 0, 0, 0, 0, 0",
+ "99999, 64, 1, 0, 1, 1, 23, 31",
+ "-99999, 64, -1, 0, -1, -1, -23, -31"
+ })
+ public void testDistributeAll(long count, int stackSize, long doubleChestShulkers, long chestShulkers,
+ long doubleChests, long chests, long stacks, long items) {
+ Set containers = EnumSet.of(Container.STACK, Container.CHEST, Container.DOUBLE_CHEST,
+ Container.CHEST_SHULKER, Container.DOUBLE_CHEST_SHULKER);
+ assertThat(new ItemCount(count, stackSize).distribute(containers))
+ .containsExactly(doubleChestShulkers, chestShulkers, doubleChests, chests, stacks, items);
+ }
+
+}