From 9502bb8566be5efe42471c8426751caa7cb8a03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Skj=C3=B8lberg?= Date: Mon, 9 Sep 2024 01:19:28 +0200 Subject: [PATCH] Add new iterator --- .../packing/api/StackableItemGroup.java | 8 + .../packager/DefaultLoadableItemFilter.java | 6 +- .../packing/api/packager/Loadable.java | 6 +- .../packing/api/packager/LoadableItem.java | 8 + .../api/packager/LoadableItemGroup.java | 70 +++ .../packing/api/packager/LoadableItems.java | 10 +- ...tractLoadableItemGroupIteratorBuilder.java | 110 ++++ .../AbstractLoadableIteratorBuilder.java | 95 ++++ ...ctLoadablePermutationRotationIterator.java | 74 +++ ...eItemGroupPermutationRotationIterator.java | 453 +++++++++++++++ ...adableItemPermutationRotationIterator.java | 355 ++++++++++++ ...adableItemPermutationRotationIterator.java | 136 +++++ .../packing/iterator/MutableLoadableItem.java | 20 + ...adableItemPermutationRotationIterator.java | 333 ++++++++++++ ...adablePermutationRotationIteratorTest.java | 514 ++++++++++++++++++ ...mGroupPermutationRotationIteratorTest.java | 345 ++++++++++++ ...leItemPermutationRotationIteratorTest.java | 12 + ...adablePermutationRotationIteratorTest.java | 167 ++++++ 18 files changed, 2713 insertions(+), 9 deletions(-) create mode 100644 api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItemGroup.java create mode 100644 core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadableItemGroupIteratorBuilder.java create mode 100644 core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadableIteratorBuilder.java create mode 100644 core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadablePermutationRotationIterator.java create mode 100644 core/src/main/java/com/github/skjolber/packing/iterator/DefaultLoadableItemGroupPermutationRotationIterator.java create mode 100644 core/src/main/java/com/github/skjolber/packing/iterator/DefaultLoadableItemPermutationRotationIterator.java create mode 100644 core/src/main/java/com/github/skjolber/packing/iterator/LoadableItemPermutationRotationIterator.java create mode 100644 core/src/main/java/com/github/skjolber/packing/iterator/MutableLoadableItem.java create mode 100644 core/src/main/java/com/github/skjolber/packing/iterator/MutableLoadableItemPermutationRotationIterator.java create mode 100644 core/src/test/java/com/github/skjolber/packing/iterator/AbstractLoadablePermutationRotationIteratorTest.java create mode 100644 core/src/test/java/com/github/skjolber/packing/iterator/DefaultLoadableItemGroupPermutationRotationIteratorTest.java create mode 100644 core/src/test/java/com/github/skjolber/packing/iterator/LoadableItemPermutationRotationIteratorTest.java create mode 100644 core/src/test/java/com/github/skjolber/packing/iterator/MutableLoadablePermutationRotationIteratorTest.java diff --git a/api/src/main/java/com/github/skjolber/packing/api/StackableItemGroup.java b/api/src/main/java/com/github/skjolber/packing/api/StackableItemGroup.java index 8fe5c0e9..f4aed522 100644 --- a/api/src/main/java/com/github/skjolber/packing/api/StackableItemGroup.java +++ b/api/src/main/java/com/github/skjolber/packing/api/StackableItemGroup.java @@ -29,4 +29,12 @@ public void setId(String id) { public void setItems(List items) { this.items = items; } + + public int size() { + return items.size(); + } + + public StackableItem get(int i) { + return items.get(i); + } } \ No newline at end of file diff --git a/api/src/main/java/com/github/skjolber/packing/api/packager/DefaultLoadableItemFilter.java b/api/src/main/java/com/github/skjolber/packing/api/packager/DefaultLoadableItemFilter.java index 9e9d0283..d3f33131 100644 --- a/api/src/main/java/com/github/skjolber/packing/api/packager/DefaultLoadableItemFilter.java +++ b/api/src/main/java/com/github/skjolber/packing/api/packager/DefaultLoadableItemFilter.java @@ -10,11 +10,7 @@ public DefaultLoadableItemFilter(LoadableItems loadableItems) { @Override public void loaded(int index) { - LoadableItem loadableItem = loadableItems.get(index); - loadableItem.decrement(); - if(loadableItem.isEmpty()) { - loadableItems.remove(index); - } + // do nothing } } \ No newline at end of file diff --git a/api/src/main/java/com/github/skjolber/packing/api/packager/Loadable.java b/api/src/main/java/com/github/skjolber/packing/api/packager/Loadable.java index 64769c25..e27f7c0f 100644 --- a/api/src/main/java/com/github/skjolber/packing/api/packager/Loadable.java +++ b/api/src/main/java/com/github/skjolber/packing/api/packager/Loadable.java @@ -19,7 +19,7 @@ public class Loadable { protected final long minVolumeLimit; protected final long minAreaLimit; - public Loadable(Stackable stackable, List stackValues, int count) { + public Loadable(Stackable stackable, List stackValues) { this.values = stackValues; this.stackable = stackable; @@ -57,4 +57,8 @@ public long getMinVolumeLimit() { public Stackable getStackable() { return stackable; } + + public StackValue getStackValue(int index) { + return values.get(index); + } } diff --git a/api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItem.java b/api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItem.java index 08faa6fb..e2d43aa0 100644 --- a/api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItem.java +++ b/api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItem.java @@ -37,4 +37,12 @@ public void decrement() { public boolean isEmpty() { return count == 0; } + + public int getIndex() { + return index; + } + + public void decrement(int value) { + this.count = this.count - value; + } } diff --git a/api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItemGroup.java b/api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItemGroup.java new file mode 100644 index 00000000..e00bbb66 --- /dev/null +++ b/api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItemGroup.java @@ -0,0 +1,70 @@ +package com.github.skjolber.packing.api.packager; + +import java.util.List; + +/** + * + * Items which belong together, for example different parts of a single product or order. + * + */ + +public class LoadableItemGroup { + + private String id; + private List items; + + public LoadableItemGroup(String id, List items) { + super(); + this.id = id; + this.items = items; + } + + public String getId() { + return id; + } + + public List getItems() { + return items; + } + + public void setId(String id) { + this.id = id; + } + + public void setItems(List items) { + this.items = items; + } + + public int size() { + return items.size(); + } + + public int loadableItemsCount() { + int count = 0; + for (LoadableItem loadableItem : items) { + count += loadableItem.getCount(); + } + return count; + } + + public boolean isEmpty() { + for (LoadableItem loadableItem : items) { + if(!loadableItem.isEmpty()) { + return false; + } + } + + return true; + } + + public void removeEmpty() { + for (int j = 0; j < items.size(); j++) { + LoadableItem loadableItem = items.get(j); + + if(loadableItem.isEmpty()) { + items.remove(j); + j--; + } + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItems.java b/api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItems.java index 3c4a78f1..66c3af90 100644 --- a/api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItems.java +++ b/api/src/main/java/com/github/skjolber/packing/api/packager/LoadableItems.java @@ -1,13 +1,17 @@ package com.github.skjolber.packing.api.packager; -import java.util.List; - /** * * The items which are available for load into some particular container. * */ -public interface LoadableItems extends List{ +public interface LoadableItems { + + int size(); + LoadableItem get(int index); + + void remove(int index, int count); + } diff --git a/core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadableItemGroupIteratorBuilder.java b/core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadableItemGroupIteratorBuilder.java new file mode 100644 index 00000000..0ee6a955 --- /dev/null +++ b/core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadableItemGroupIteratorBuilder.java @@ -0,0 +1,110 @@ +package com.github.skjolber.packing.iterator; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import com.github.skjolber.packing.api.Dimension; +import com.github.skjolber.packing.api.StackValue; +import com.github.skjolber.packing.api.Stackable; +import com.github.skjolber.packing.api.StackableItem; +import com.github.skjolber.packing.api.StackableItemGroup; +import com.github.skjolber.packing.api.packager.Loadable; +import com.github.skjolber.packing.api.packager.LoadableItem; +import com.github.skjolber.packing.api.packager.LoadableItemGroup; + +/** + * Builder scaffold. + * + * @see https://www.sitepoint.com/self-types-with-javas-generics/ + */ + +public abstract class AbstractLoadableItemGroupIteratorBuilder> { + + protected int maxLoadWeight = -1; + protected Predicate filter; + protected Dimension size; + protected List stackableItemGroups; + + public B withSize(int dx, int dy, int dz) { + this.size = new Dimension(dx, dy, dz); + + return (B)this; + } + + public B withLoadSize(Dimension dimension) { + this.size = dimension; + + return (B)this; + } + + public B withFilter(Predicate filter) { + this.filter = filter; + + return (B)this; + } + + public B withMaxLoadWeight(int maxLoadWeight) { + this.maxLoadWeight = maxLoadWeight; + + return (B)this; + } + + public B withStackableItemGroups(List stackableItems) { + this.stackableItemGroups = stackableItems; + + return (B)this; + } + + protected List toMatrix() { + List results = new ArrayList<>(stackableItemGroups.size()); + + int offset = 0; + + for (int i = 0; i < stackableItemGroups.size(); i++) { + + StackableItemGroup group = stackableItemGroups.get(i); + + List loadableItems = new ArrayList<>(group.size()); + for (int k = 0; k < group.size(); k++) { + StackableItem item = group.get(k); + + if(item.getCount() == 0) { + continue; + } + + Stackable stackable = item.getStackable(); + if(stackable.getWeight() > maxLoadWeight) { + continue; + } + + if(stackable.getVolume() > size.getVolume()) { + continue; + } + + List boundRotations = stackable.rotations(size); + if(boundRotations == null || boundRotations.isEmpty()) { + continue; + } + + if(filter != null && !filter.test(stackable)) { + continue; + } + + Loadable loadable = new Loadable(stackable, boundRotations); + + loadableItems.add(new LoadableItem(loadable, item.getCount(), offset)); + + offset++; + } + if(!loadableItems.isEmpty()) { + results.add(new LoadableItemGroup(group.getId(), loadableItems)); + } + } + return results; + } + + public abstract LoadableItemPermutationRotationIterator build(); + +} diff --git a/core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadableIteratorBuilder.java b/core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadableIteratorBuilder.java new file mode 100644 index 00000000..285457cf --- /dev/null +++ b/core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadableIteratorBuilder.java @@ -0,0 +1,95 @@ +package com.github.skjolber.packing.iterator; + +import java.util.List; +import java.util.function.Predicate; + +import com.github.skjolber.packing.api.Dimension; +import com.github.skjolber.packing.api.StackValue; +import com.github.skjolber.packing.api.Stackable; +import com.github.skjolber.packing.api.StackableItem; +import com.github.skjolber.packing.api.packager.Loadable; +import com.github.skjolber.packing.api.packager.LoadableItem; +import com.github.skjolber.packing.api.packager.LoadableItems; + +/** + * Builder scaffold. + * + * @see https://www.sitepoint.com/self-types-with-javas-generics/ + */ + +public abstract class AbstractLoadableIteratorBuilder> { + + protected int maxLoadWeight = -1; + protected Predicate filter; + protected Dimension size; + protected List stackableItems; + + public B withSize(int dx, int dy, int dz) { + this.size = new Dimension(dx, dy, dz); + + return (B)this; + } + + public B withLoadSize(Dimension dimension) { + this.size = dimension; + + return (B)this; + } + + public B withFilter(Predicate filter) { + this.filter = filter; + + return (B)this; + } + + public B withMaxLoadWeight(int maxLoadWeight) { + this.maxLoadWeight = maxLoadWeight; + + return (B)this; + } + + public B withStackableItems(List stackableItems) { + this.stackableItems = stackableItems; + + return (B)this; + } + + protected LoadableItem[] toMatrix() { + LoadableItem[] results = new LoadableItem[stackableItems.size()]; + + for (int i = 0; i < stackableItems.size(); i++) { + StackableItem item = stackableItems.get(i); + + if(item.getCount() == 0) { + continue; + } + + Stackable stackable = item.getStackable(); + if(stackable.getWeight() > maxLoadWeight) { + continue; + } + + if(stackable.getVolume() > size.getVolume()) { + continue; + } + + List boundRotations = stackable.rotations(size); + if(boundRotations == null || boundRotations.isEmpty()) { + continue; + } + + if(filter != null && !filter.test(stackable)) { + continue; + } + + Loadable loadable = new Loadable(stackable, boundRotations); + + results[i] = new LoadableItem(loadable, item.getCount(), i); + } + return results; + } + + public abstract LoadableItemPermutationRotationIterator build(); + +} diff --git a/core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadablePermutationRotationIterator.java b/core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadablePermutationRotationIterator.java new file mode 100644 index 00000000..34db4ee4 --- /dev/null +++ b/core/src/main/java/com/github/skjolber/packing/iterator/AbstractLoadablePermutationRotationIterator.java @@ -0,0 +1,74 @@ +package com.github.skjolber.packing.iterator; + +import java.util.ArrayList; +import java.util.List; + +import com.github.skjolber.packing.api.StackValue; +import com.github.skjolber.packing.api.StackableItem; +import com.github.skjolber.packing.api.packager.LoadableItem; + +public abstract class AbstractLoadablePermutationRotationIterator implements LoadableItemPermutationRotationIterator { + + protected final LoadableItem[] loadableItems; // by index + protected int[] reset; + + public AbstractLoadablePermutationRotationIterator(LoadableItem[] matrix) { + this.loadableItems = matrix; + } + + public LoadableItem[] getMatrix() { + return loadableItems; + } + + /** + * Get number of box items within the constraints. + * + * @return number between 0 and number of {@linkplain StackableItem}s used in the constructor. + */ + + public int boxItemLength() { + return loadableItems.length; + } + + public long getMinStackableArea(int offset) { + long minArea = Long.MAX_VALUE; + for (int i = offset; i < length(); i++) { + StackValue permutationRotation = getStackValue(i); + long area = permutationRotation.getArea(); + if(area < minArea) { + minArea = area; + } + } + return minArea; + } + + public int getMinStackableAreaIndex(int offset) { + long minArea = getStackValue(offset).getArea(); + int index = offset; + + for (int i = offset + 1; i < length(); i++) { + StackValue permutationRotation = getStackValue(i); + long area = permutationRotation.getArea(); + if(area < minArea) { + minArea = area; + index = i; + } + } + return index; + } + + public List get(PermutationRotationState state, int length) { + int[] permutations = state.getPermutations(); + int[] rotations = state.getRotations(); + + List results = new ArrayList(length); + for (int i = 0; i < length; i++) { + results.add(loadableItems[permutations[i]].getLoadable().getStackValue(rotations[i])); + } + return results; + } + + public abstract int length(); + + public abstract StackValue getStackValue(int index); +} diff --git a/core/src/main/java/com/github/skjolber/packing/iterator/DefaultLoadableItemGroupPermutationRotationIterator.java b/core/src/main/java/com/github/skjolber/packing/iterator/DefaultLoadableItemGroupPermutationRotationIterator.java new file mode 100644 index 00000000..6465a66e --- /dev/null +++ b/core/src/main/java/com/github/skjolber/packing/iterator/DefaultLoadableItemGroupPermutationRotationIterator.java @@ -0,0 +1,453 @@ +package com.github.skjolber.packing.iterator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.github.skjolber.packing.api.StackValue; +import com.github.skjolber.packing.api.packager.LoadableItem; +import com.github.skjolber.packing.api.packager.LoadableItemGroup; + +public class DefaultLoadableItemGroupPermutationRotationIterator extends AbstractLoadablePermutationRotationIterator { + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder extends AbstractLoadableItemGroupIteratorBuilder { + + public DefaultLoadableItemGroupPermutationRotationIterator build() { + if(maxLoadWeight == -1) { + throw new IllegalStateException(); + } + if(size == null) { + throw new IllegalStateException(); + } + + List groups = toMatrix(); + + List matrix = new ArrayList<>(); + for (LoadableItemGroup loadableItemGroup : groups) { + matrix.addAll(loadableItemGroup.getItems()); + } + + return new DefaultLoadableItemGroupPermutationRotationIterator(groups, matrix.toArray(new LoadableItem[matrix.size()])); + } + + } + + protected int[] rotations; // 2^n or 6^n + + // permutations of boxes that fit inside this container + protected int[] permutations; // n! + + // minimum volume from index i and above + protected long[] minStackableVolume; + + protected List groups; + + public DefaultLoadableItemGroupPermutationRotationIterator(List groups, LoadableItem[] matrix) { + super(matrix); + + this.groups = groups; + + int count = 0; + + for (LoadableItem loadableItem : matrix) { + if(loadableItem != null) { + count += loadableItem.getCount(); + } + } + + this.minStackableVolume = new long[count]; + + initiatePermutation(count); + } + + public StackValue getStackValue(int index) { + return loadableItems[permutations[index]].getLoadable().getStackValue(rotations[index]); + } + + public void removePermutations(int count) { + // discard a number of items from the front + for(int i = 0; i < count; i++) { + LoadableItem loadableItem = loadableItems[permutations[i]]; + + loadableItem.decrement(); + + if(loadableItem.isEmpty()) { + loadableItems[i] = null; + } + } + + for(int i = 0; i < groups.size(); i++) { + LoadableItemGroup group = groups.get(i); + + group.removeEmpty(); + + if(group.isEmpty()) { + groups.remove(i); + i--; + } else { + break; + } + } + + initiatePermutation(rotations.length - count); + } + + protected void initiatePermutation(int remainingCount) { + this.rotations = new int[remainingCount]; + this.reset = new int[rotations.length]; + + // need to be in ascending order for the algorithm to work + int[] permutations = new int[rotations.length]; + + int offset = 0; + for (int j = 0; j < loadableItems.length; j++) { + LoadableItem value = loadableItems[j]; + if(value != null && !value.isEmpty()) { + for (int k = 0; k < value.getCount(); k++) { + permutations[offset] = j; + offset++; + } + } + } + + this.permutations = permutations; + + if(permutations.length > 0) { + calculateMinStackableVolume(0); + } + } + + protected void calculateMinStackableVolume(int offset) { + StackValue last = loadableItems[permutations[permutations.length - 1]].getLoadable().getStackValue(rotations[permutations.length - 1]); + + minStackableVolume[permutations.length - 1] = last.getVolume(); + + for (int i = permutations.length - 2; i >= offset; i--) { + long volume = loadableItems[permutations[i]].getLoadable().getStackValue(rotations[i]).getVolume(); + + if(volume < minStackableVolume[i + 1]) { + minStackableVolume[i] = volume; + } else { + minStackableVolume[i] = minStackableVolume[i + 1]; + } + } + } + + public long getMinStackableVolume(int offset) { + return minStackableVolume[offset]; + } + + protected long[] getMinStackableVolume() { + return minStackableVolume; + } + + + /** + * Remove permutations, if present. + */ + + public void removePermutations(List removed) { + + for (Integer i : removed) { + LoadableItem loadableItem = loadableItems[i]; + + loadableItem.decrement(); + + if(loadableItem.isEmpty()) { + loadableItems[i] = null; + } + } + + // go through all groups and clean up + for(int i = 0; i < groups.size(); i++) { + LoadableItemGroup group = groups.get(i); + + group.removeEmpty(); + if(group.isEmpty()) { + groups.remove(i); + i--; + } + } + + initiatePermutation(rotations.length - removed.size()); + } + + + public int nextRotation() { + // next rotation + return nextRotation(rotations.length - 1); + } + + public int nextRotation(int maxIndex) { + // next rotation + for (int i = maxIndex; i >= 0; i--) { + if(rotations[i] < loadableItems[permutations[i]].getLoadable().getValues().size() - 1) { + rotations[i]++; + + System.arraycopy(reset, 0, rotations, i + 1, rotations.length - (i + 1)); + + return i; + } + } + + return -1; + } + + + public int[] getPermutations() { + return permutations; + } + + protected void resetRotations() { + System.arraycopy(reset, 0, rotations, 0, rotations.length); + } + + public long countRotations() { + long n = 1; + for (int i = 0; i < permutations.length; i++) { + LoadableItem value = loadableItems[permutations[i]]; + if(Long.MAX_VALUE / value.getLoadable().getValues().size() <= n) { + return -1L; + } + + n = n * value.getLoadable().getValues().size(); + } + return n; + } + + /** + * Return number of permutations for boxes which fit within this container. + * + * @return permutation count + */ + + public long countPermutations() { + // reduce permutations for boxes which are duplicated + + // could be further bounded by looking at how many boxes (i.e. n x the smallest) which actually + // fit within the container volume + long n = 1; + + for (LoadableItemGroup loadableItemGroup : groups) { + + List items = loadableItemGroup.getItems(); + + int count = loadableItemGroup.loadableItemsCount(); + + int maxCount = 0; + for (LoadableItem value : items) { + if(value != null) { + if(maxCount < value.getCount()) { + maxCount = value.getCount(); + } + } + } + + if(maxCount > 1) { + int[] factors = new int[maxCount]; + for (LoadableItem value : items) { + if(value != null) { + for (int k = 0; k < value.getCount(); k++) { + factors[k]++; + } + } + } + + for (long i = 0; i < count; i++) { + if(Long.MAX_VALUE / (i + 1) <= n) { + return -1L; + } + + n = n * (i + 1); + + for (int k = 1; k < maxCount; k++) { + while (factors[k] > 0 && n % (k + 1) == 0) { + n = n / (k + 1); + + factors[k]--; + } + } + } + + for (int k = 1; k < maxCount; k++) { + while (factors[k] > 0) { + n = n / (k + 1); + + factors[k]--; + } + } + } else { + for (long i = 0; i < count; i++) { + if(Long.MAX_VALUE / (i + 1) <= n) { + return -1L; + } + n = n * (i + 1); + } + } + } + return n; + } + + public int nextPermutation(int maxIndex) { + + int limit = permutations.length; + + for(int g = groups.size() - 1; g >= 0; g--) { + LoadableItemGroup loadableItemGroup = groups.get(g); + + // Find longest non-increasing suffix + int startIndex = limit - loadableItemGroup.loadableItemsCount(); + + if(startIndex <= maxIndex && maxIndex < limit) { + while (maxIndex >= startIndex) { + + int[] permutations = this.permutations; + + int current = permutations[maxIndex]; + + // find the lexicographically next item to the right of the max index + int minIndex = -1; + for (int i = maxIndex + 1; i < limit; i++) { + if(permutations[i] > current && (minIndex == -1 || permutations[i] < permutations[minIndex])) { + minIndex = i; + } + } + + // if there is no such item, decrement and try again + if(minIndex == -1) { + // TODO search backwards? + maxIndex--; + + continue; + } + + // increment to the next lexigrapically item + // and sort the items to the right of the max index + permutations[maxIndex] = permutations[minIndex]; + permutations[minIndex] = current; + + Arrays.sort(permutations, maxIndex + 1, limit); + + resetRotations(); + + calculateMinStackableVolume(maxIndex); + + return maxIndex; + } + } + // reset current group + // TODO system arraycopy? + int i = startIndex; + + for (LoadableItem loadableItem : loadableItemGroup.getItems()) { + for(int k = 0; k < loadableItem.getCount(); k++) { + permutations[i] = loadableItem.getIndex(); + + i++; + } + } + + // skip to next group + limit = startIndex; + } + + + return -1; + } + + + public int nextPermutation() { + resetRotations(); + + int endIndex = permutations.length - 1; + + for(int g = groups.size() - 1; g >= 0; g--) { + + LoadableItemGroup loadableItemGroup = groups.get(g); + + int[] permutations = this.permutations; + + // Find longest non-increasing suffix + int i = endIndex; + int startIndex = endIndex - loadableItemGroup.loadableItemsCount() + 1; + + while (i > startIndex && permutations[i - 1] >= permutations[i]) + i--; + // Now i is the head index of the suffix + + // Are we at the last permutation already? + if(i <= startIndex) { + // reset current group + // TODO system arraycopy? + i = startIndex; + + for (LoadableItem loadableItem : loadableItemGroup.getItems()) { + for(int k = 0; k < loadableItem.getCount(); k++) { + permutations[i] = loadableItem.getIndex(); + + i++; + } + } + + // skip to next group + endIndex = startIndex - 1; + + continue; + } + + // Let array[i - 1] be the pivot + // Find rightmost element that exceeds the pivot + int j = endIndex; + while (permutations[j] <= permutations[i - 1]) + j--; + // Now the value array[j] will become the new pivot + // Assertion: j >= i + + int head = i - 1; + + // Swap the pivot with j + int temp = permutations[i - 1]; + permutations[i - 1] = permutations[j]; + permutations[j] = temp; + + // Reverse the suffix + j = endIndex; + while (i < j) { + temp = permutations[i]; + permutations[i] = permutations[j]; + permutations[j] = temp; + i++; + j--; + } + + g++; + + calculateMinStackableVolume(head); + + // Successfully computed the next permutation + return head; + } + + return -1; + } + + public int length() { + return permutations.length; + } + + public PermutationRotationState getState() { + return new PermutationRotationState(rotations, permutations); + } + + public LoadableItem getPermutation(int index) { + return loadableItems[permutations[index]]; + } + + protected int[] getRotations() { + return rotations; + } +} diff --git a/core/src/main/java/com/github/skjolber/packing/iterator/DefaultLoadableItemPermutationRotationIterator.java b/core/src/main/java/com/github/skjolber/packing/iterator/DefaultLoadableItemPermutationRotationIterator.java new file mode 100644 index 00000000..1c597101 --- /dev/null +++ b/core/src/main/java/com/github/skjolber/packing/iterator/DefaultLoadableItemPermutationRotationIterator.java @@ -0,0 +1,355 @@ +package com.github.skjolber.packing.iterator; + +import java.util.Arrays; +import java.util.List; + +import com.github.skjolber.packing.api.StackValue; +import com.github.skjolber.packing.api.packager.LoadableItem; + +public class DefaultLoadableItemPermutationRotationIterator extends AbstractLoadablePermutationRotationIterator { + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder extends AbstractLoadableIteratorBuilder { + + public DefaultLoadableItemPermutationRotationIterator build() { + if(maxLoadWeight == -1) { + throw new IllegalStateException(); + } + if(size == null) { + throw new IllegalStateException(); + } + + LoadableItem[] matrix = toMatrix(); + + return new DefaultLoadableItemPermutationRotationIterator(matrix); + } + + } + + protected int[] rotations; // 2^n or 6^n + + // permutations of boxes that fit inside this container + protected int[] permutations; // n! + + // minimum volume from index i and above + protected long[] minStackableVolume; + + public DefaultLoadableItemPermutationRotationIterator(LoadableItem[] matrix) { + super(matrix); + + int count = 0; + + for (LoadableItem loadableItem : matrix) { + if(loadableItem != null) { + count += loadableItem.getCount(); + } + } + + this.minStackableVolume = new long[count]; + + initiatePermutation(count); + } + + public StackValue getStackValue(int index) { + return loadableItems[permutations[index]].getLoadable().getStackValue(rotations[index]); + } + + public void removePermutations(int count) { + // discard a number of items from the front + for(int i = 0; i < count; i++) { + LoadableItem loadableItem = loadableItems[permutations[i]]; + + loadableItem.decrement(); + + if(loadableItem.isEmpty()) { + loadableItems[i] = null; + } + } + + initiatePermutation(rotations.length - count); + } + + protected void initiatePermutation(int remainingCount) { + this.rotations = new int[remainingCount]; + this.reset = new int[rotations.length]; + + // need to be in ascending order for the algorithm to work + int[] permutations = new int[rotations.length]; + + int offset = 0; + for (int j = 0; j < loadableItems.length; j++) { + LoadableItem value = loadableItems[j]; + if(value != null && !value.isEmpty()) { + for (int k = 0; k < value.getCount(); k++) { + permutations[offset] = j; + offset++; + } + } + } + + this.permutations = permutations; + + if(permutations.length > 0) { + calculateMinStackableVolume(0); + } + } + + protected void calculateMinStackableVolume(int offset) { + StackValue last = loadableItems[permutations[permutations.length - 1]].getLoadable().getStackValue(rotations[permutations.length - 1]); + + minStackableVolume[permutations.length - 1] = last.getVolume(); + + for (int i = permutations.length - 2; i >= offset; i--) { + long volume = loadableItems[permutations[i]].getLoadable().getStackValue(rotations[i]).getVolume(); + + if(volume < minStackableVolume[i + 1]) { + minStackableVolume[i] = volume; + } else { + minStackableVolume[i] = minStackableVolume[i + 1]; + } + } + } + + public long getMinStackableVolume(int offset) { + return minStackableVolume[offset]; + } + + protected long[] getMinStackableVolume() { + return minStackableVolume; + } + + + /** + * Remove permutations, if present. + */ + + public void removePermutations(List removed) { + + for (Integer i : removed) { + LoadableItem loadableItem = loadableItems[i]; + + loadableItem.decrement(); + + if(loadableItem.isEmpty()) { + loadableItems[i] = null; + } + } + + initiatePermutation(rotations.length - removed.size()); + } + + + public int nextRotation() { + // next rotation + return nextRotation(rotations.length - 1); + } + + public int nextRotation(int maxIndex) { + // next rotation + for (int i = maxIndex; i >= 0; i--) { + if(rotations[i] < loadableItems[permutations[i]].getLoadable().getValues().size() - 1) { + rotations[i]++; + + System.arraycopy(reset, 0, rotations, i + 1, rotations.length - (i + 1)); + + return i; + } + } + + return -1; + } + + + public int[] getPermutations() { + return permutations; + } + + protected void resetRotations() { + System.arraycopy(reset, 0, rotations, 0, rotations.length); + } + + public long countRotations() { + long n = 1; + for (int i = 0; i < permutations.length; i++) { + LoadableItem value = loadableItems[permutations[i]]; + if(Long.MAX_VALUE / value.getLoadable().getValues().size() <= n) { + return -1L; + } + + n = n * value.getLoadable().getValues().size(); + } + return n; + } + + /** + * Return number of permutations for boxes which fit within this container. + * + * @return permutation count + */ + + public long countPermutations() { + // reduce permutations for boxes which are duplicated + + // could be further bounded by looking at how many boxes (i.e. n x the smallest) which actually + // fit within the container volume + + int maxCount = 0; + for (LoadableItem value : loadableItems) { + if(value != null) { + if(maxCount < value.getCount()) { + maxCount = value.getCount(); + } + } + } + + long n = 1; + if(maxCount > 1) { + int[] factors = new int[maxCount]; + for (LoadableItem value : loadableItems) { + if(value != null) { + for (int k = 0; k < value.getCount(); k++) { + factors[k]++; + } + } + } + + for (long i = 0; i < permutations.length; i++) { + if(Long.MAX_VALUE / (i + 1) <= n) { + return -1L; + } + + n = n * (i + 1); + + for (int k = 1; k < maxCount; k++) { + while (factors[k] > 0 && n % (k + 1) == 0) { + n = n / (k + 1); + + factors[k]--; + } + } + } + + for (int k = 1; k < maxCount; k++) { + while (factors[k] > 0) { + n = n / (k + 1); + + factors[k]--; + } + } + } else { + for (long i = 0; i < permutations.length; i++) { + if(Long.MAX_VALUE / (i + 1) <= n) { + return -1L; + } + n = n * (i + 1); + } + } + return n; + } + + public int nextPermutation(int maxIndex) { + while (maxIndex >= 0) { + + int[] permutations = this.permutations; + + int current = permutations[maxIndex]; + + // find the lexicographically next item to the right of the max index + int minIndex = -1; + for (int i = maxIndex + 1; i < permutations.length; i++) { + if(permutations[i] > current && (minIndex == -1 || permutations[i] < permutations[minIndex])) { + minIndex = i; + } + } + + // if there is no such item, decrement and try again + if(minIndex == -1) { + // TODO search backwards? + maxIndex--; + + continue; + } + + // increment to the next lexigrapically item + // and sort the items to the right of the max index + permutations[maxIndex] = permutations[minIndex]; + permutations[minIndex] = current; + + Arrays.sort(permutations, maxIndex + 1, permutations.length); + + resetRotations(); + + calculateMinStackableVolume(maxIndex); + + return maxIndex; + } + return -1; + } + + + public int nextPermutation() { + resetRotations(); + + int[] permutations = this.permutations; + + // Find longest non-increasing suffix + int i = permutations.length - 1; + while (i > 0 && permutations[i - 1] >= permutations[i]) + i--; + // Now i is the head index of the suffix + + // Are we at the last permutation already? + if(i <= 0) { + return -1; + } + + // Let array[i - 1] be the pivot + // Find rightmost element that exceeds the pivot + int j = permutations.length - 1; + while (permutations[j] <= permutations[i - 1]) + j--; + // Now the value array[j] will become the new pivot + // Assertion: j >= i + + int head = i - 1; + + // Swap the pivot with j + int temp = permutations[i - 1]; + permutations[i - 1] = permutations[j]; + permutations[j] = temp; + + // Reverse the suffix + j = permutations.length - 1; + while (i < j) { + temp = permutations[i]; + permutations[i] = permutations[j]; + permutations[j] = temp; + i++; + j--; + } + + calculateMinStackableVolume(head); + + // Successfully computed the next permutation + return head; + } + + public int length() { + return permutations.length; + } + + public PermutationRotationState getState() { + return new PermutationRotationState(rotations, permutations); + } + + public LoadableItem getPermutation(int index) { + return loadableItems[permutations[index]]; + } + + protected int[] getRotations() { + return rotations; + } +} diff --git a/core/src/main/java/com/github/skjolber/packing/iterator/LoadableItemPermutationRotationIterator.java b/core/src/main/java/com/github/skjolber/packing/iterator/LoadableItemPermutationRotationIterator.java new file mode 100644 index 00000000..590bb2e6 --- /dev/null +++ b/core/src/main/java/com/github/skjolber/packing/iterator/LoadableItemPermutationRotationIterator.java @@ -0,0 +1,136 @@ +package com.github.skjolber.packing.iterator; + +import java.util.List; + +import com.github.skjolber.packing.api.StackValue; + +/** + * + * Rotation and permutations built into the same interface. Minimizes the number of + * rotations.
+ *
+ * The maximum number of combinations is n! * 6^n, however after accounting for + * bounds and sides with equal lengths the number can be a lot lower (and this + * number can be obtained before starting the calculation).
+ *
+ * Note that permutations are for the boxes which actually fit within this container. + *
+ *
+ * Assumes a do-while approach: + * + *
+ * {@code
+ * do {
+ * 	do {
+ * 		for (int i = 0; i < n; i++) {
+ * 			PermutationRotation box = instance.get(i);
+ * 			// .. your code here
+ * 		}
+ * 	} while (instance.nextRotation() != -1);
+ * } while (instance.nextPermutation() != -1);
+ *
+ * }
+ * 
+ * + * @see next-lexicographical-permutation-algorithm + */ + +public interface LoadableItemPermutationRotationIterator { + + /** + * + * Get current length + * + * @return current length of permutations array + */ + + int length(); + + StackValue getStackValue(int index); + + /** + * Get current state + * + * @return current state + */ + + PermutationRotationState getState(); + + /** + * + * Get permutations + rotations for a state + * + * @param state previously saved state + * @param length number of items + * @return current permutations + rotations + */ + + List get(PermutationRotationState state, int length); + + long getMinStackableVolume(int index); + + int getMinStackableAreaIndex(int i); + + /** + * Get current permutations + * + * @return current permutations array + */ + + int[] getPermutations(); + + long countRotations(); + + long countPermutations(); + + // write access methods + + + /** + * Next rotation. + * + * @return change index, or -1 if none + */ + + int nextRotation(); + + /** + * Next rotation. Returns the index of the lowest element which as affected. + * + * @param maxIndex skip ahead so that rotation affects the argument at index or lower. + * @return change index, or -1 if none + */ + + int nextRotation(int maxIndex); + + /** + * Next permutation. + * + * @return change index, or -1 if none + */ + + int nextPermutation(); + + /** + * Next permutation. Returns the index of the lowest element which as affected. + * + * @param maxIndex skip ahead so that permutation affects the argument at index or lower. + * @return change index, or -1 if none + */ + + int nextPermutation(int maxIndex); + + + /** + * Remove permutations, if present. + * + * @param removed list of permutation indexes to remove + */ + + void removePermutations(List removed); + + void removePermutations(int count); + +} \ No newline at end of file diff --git a/core/src/main/java/com/github/skjolber/packing/iterator/MutableLoadableItem.java b/core/src/main/java/com/github/skjolber/packing/iterator/MutableLoadableItem.java new file mode 100644 index 00000000..ffce5b50 --- /dev/null +++ b/core/src/main/java/com/github/skjolber/packing/iterator/MutableLoadableItem.java @@ -0,0 +1,20 @@ +package com.github.skjolber.packing.iterator; + +import com.github.skjolber.packing.api.packager.LoadableItem; + +public class MutableLoadableItem extends LoadableItem { + + public final LoadableItem source; + + public MutableLoadableItem(LoadableItem loadableItem) { + super(loadableItem.getLoadable(), loadableItem.getCount(), loadableItem.getIndex()); + + this.source = loadableItem; + } + + public void reset() { + this.count = source.getCount(); + } + + +} diff --git a/core/src/main/java/com/github/skjolber/packing/iterator/MutableLoadableItemPermutationRotationIterator.java b/core/src/main/java/com/github/skjolber/packing/iterator/MutableLoadableItemPermutationRotationIterator.java new file mode 100644 index 00000000..901cf498 --- /dev/null +++ b/core/src/main/java/com/github/skjolber/packing/iterator/MutableLoadableItemPermutationRotationIterator.java @@ -0,0 +1,333 @@ +package com.github.skjolber.packing.iterator; + +import java.util.ArrayList; +import java.util.List; + +import com.github.skjolber.packing.api.StackValue; +import com.github.skjolber.packing.api.packager.LoadableItem; +import com.github.skjolber.packing.api.packager.LoadableItems; + + /** + * + * An iterator which also acts as Loadable items. + * + * State is restored on each remove, next rotation or next permutation. + * + */ + + +public class MutableLoadableItemPermutationRotationIterator implements LoadableItems, LoadableItemPermutationRotationIterator { + + public static Builder newMutableBuilder() { + return new Builder(); + } + + public static class Builder extends AbstractLoadableIteratorBuilder { + + public MutableLoadableItemPermutationRotationIterator build() { + if(maxLoadWeight == -1) { + throw new IllegalStateException(); + } + if(size == null) { + throw new IllegalStateException(); + } + + LoadableItem[] matrix = toMatrix(); + + return new MutableLoadableItemPermutationRotationIterator(matrix); + } + } + + protected int[] mutableRotations; // 2^n or 6^n + + // permutations of boxes that fit inside this container + protected int[] mutablePermutations = new int[0]; // n! + + // minimum volume from index i and above + protected long[] mutableMinStackableVolume; + + protected List mutableLoadableItems; + + protected final DefaultLoadableItemPermutationRotationIterator iterator; + + protected final LoadableItem[] loadableItems; // by index + + public MutableLoadableItemPermutationRotationIterator(LoadableItem[] loadableItems) { + iterator = new DefaultLoadableItemPermutationRotationIterator(loadableItems); + + this.loadableItems = loadableItems; + + resetFromIterator(); + } + + protected void resetFromIterator() { + int[] permutations = iterator.getPermutations(); + + mutableLoadableItems = new ArrayList<>(); + for (int i = 0; i < loadableItems.length; i++) { + LoadableItem loadableItem = loadableItems[i]; + if(loadableItem != null && !loadableItem.isEmpty()) { + mutableLoadableItems.add(new MutableLoadableItem(loadableItem)); + } + } + + mutablePermutations = new int[permutations.length]; + mutableRotations = new int[permutations.length]; + mutableMinStackableVolume = new long[permutations.length]; + + System.arraycopy(permutations, 0, mutablePermutations, 0, permutations.length); + System.arraycopy(iterator.getMinStackableVolume(), 0, mutableMinStackableVolume, 0, permutations.length); + } + + + public LoadableItem get(int index) { + return mutableLoadableItems.get(index); + } + + @Override + public StackValue getStackValue(int index) { + return loadableItems[mutablePermutations[index]].getLoadable().getStackValue(mutableRotations[index]); + } + + @Override + public int size() { + return mutableLoadableItems.size(); + } + + @Override + public int length() { + return mutablePermutations.length; + } + + @Override + public int nextPermutation(int maxIndex) { + int result = iterator.nextPermutation(maxIndex); + if(result != -1) { + resetFromIterator(); + } + return result; + } + + @Override + public int nextRotation(int maxIndex) { + int result = iterator.nextRotation(maxIndex); + if(result != -1) { + resetFromIterator(); + } + return result; + } + + @Override + public void remove(int index, int count) { + LoadableItem loadableItem = mutableLoadableItems.get(index); + loadableItem.decrement(count); + + if(loadableItem.isEmpty()) { + mutableLoadableItems.remove(index); + } + + int remainingCount = mutablePermutations.length - count; + + // make inline changes, do not reset + int[] permutations = new int[remainingCount]; + int[] rotations = new int[remainingCount]; + + int offset = 0; + for(int i = 0; i < mutablePermutations.length; i++) { + if(mutablePermutations[i] == loadableItem.getIndex() && count > 0) { + count--; + } else { + permutations[offset] = mutablePermutations[i]; + rotations[offset] = mutableRotations[i]; + + offset++; + } + } + + this.mutablePermutations = permutations; + this.mutableRotations = rotations; + + if(remainingCount > 0) { + calculateMutableMinStackableVolume(0); + } + } + + private void calculateMutableMinStackableVolume(int offset) { + StackValue last = getStackValue(mutablePermutations.length - 1); + + mutableMinStackableVolume[mutablePermutations.length - 1] = last.getVolume(); + + for (int i = mutablePermutations.length - 2; i >= offset; i--) { + long volume = getStackValue(i).getVolume(); + + if(volume < mutableMinStackableVolume[i + 1]) { + mutableMinStackableVolume[i] = volume; + } else { + mutableMinStackableVolume[i] = mutableMinStackableVolume[i + 1]; + } + } + } + + public PermutationRotationState getState() { + return new PermutationRotationState(mutableRotations, mutablePermutations); + } + + @Override + public int[] getPermutations() { + return mutablePermutations; + } + + public List get(PermutationRotationState state, int length) { + return iterator.get(state, length); + } + + public long getMinStackableArea(int offset) { + long minArea = Long.MAX_VALUE; + for (int i = offset; i < length(); i++) { + StackValue permutationRotation = getStackValue(i); + long area = permutationRotation.getArea(); + if(area < minArea) { + minArea = area; + } + } + return minArea; + } + + public int getMinStackableAreaIndex(int offset) { + long minArea = getStackValue(offset).getArea(); + int index = offset; + + for (int i = offset + 1; i < length(); i++) { + StackValue permutationRotation = getStackValue(i); + long area = permutationRotation.getArea(); + if(area < minArea) { + minArea = area; + index = i; + } + } + return index; + } + + public long getMinStackableVolume(int offset) { + return mutableMinStackableVolume[offset]; + } + + protected long[] getMinStackableVolume() { + return mutableMinStackableVolume; + } + + @Override + public int nextRotation() { + int result = iterator.nextRotation(); + if(result != -1) { + resetFromIterator(); + } + return result; + } + + @Override + public int nextPermutation() { + int result = iterator.nextPermutation(); + if(result != -1) { + resetFromIterator(); + } + return result; + } + + @Override + public void removePermutations(List removed) { + iterator.removePermutations(removed); + + resetFromIterator(); + } + + @Override + public long countRotations() { + return iterator.countRotations(); + } + + public long countMutableRotations() { + long n = 1; + for (int i = 0; i < mutablePermutations.length; i++) { + LoadableItem value = loadableItems[mutablePermutations[i]]; + if(Long.MAX_VALUE / value.getLoadable().getValues().size() <= n) { + return -1L; + } + + n = n * value.getLoadable().getValues().size(); + } + return n; + } + + @Override + public long countPermutations() { + return iterator.countPermutations(); + } + + public long countMutablePermutations() { + // reduce permutations for boxes which are duplicated + + // could be further bounded by looking at how many boxes (i.e. n x the smallest) which actually + // fit within the container volume + + int maxCount = 0; + for (LoadableItem value : loadableItems) { + if(value != null) { + if(maxCount < value.getCount()) { + maxCount = value.getCount(); + } + } + } + + long n = 1; + if(maxCount > 1) { + int[] factors = new int[maxCount]; + for (LoadableItem value : loadableItems) { + if(value != null) { + for (int k = 0; k < value.getCount(); k++) { + factors[k]++; + } + } + } + + for (long i = 0; i < mutablePermutations.length; i++) { + if(Long.MAX_VALUE / (i + 1) <= n) { + return -1L; + } + + n = n * (i + 1); + + for (int k = 1; k < maxCount; k++) { + while (factors[k] > 0 && n % (k + 1) == 0) { + n = n / (k + 1); + + factors[k]--; + } + } + } + + for (int k = 1; k < maxCount; k++) { + while (factors[k] > 0) { + n = n / (k + 1); + + factors[k]--; + } + } + } else { + for (long i = 0; i < mutablePermutations.length; i++) { + if(Long.MAX_VALUE / (i + 1) <= n) { + return -1L; + } + n = n * (i + 1); + } + } + return n; + } + + public void removePermutations(int removed) { + iterator.removePermutations(removed); + + resetFromIterator(); + } + +} diff --git a/core/src/test/java/com/github/skjolber/packing/iterator/AbstractLoadablePermutationRotationIteratorTest.java b/core/src/test/java/com/github/skjolber/packing/iterator/AbstractLoadablePermutationRotationIteratorTest.java new file mode 100644 index 00000000..ff076307 --- /dev/null +++ b/core/src/test/java/com/github/skjolber/packing/iterator/AbstractLoadablePermutationRotationIteratorTest.java @@ -0,0 +1,514 @@ +package com.github.skjolber.packing.iterator; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.github.skjolber.packing.api.Box; +import com.github.skjolber.packing.api.Dimension; +import com.github.skjolber.packing.api.StackValue; +import com.github.skjolber.packing.api.StackableItem; + +public abstract class AbstractLoadablePermutationRotationIteratorTest { + + protected static void assertMinStackableVolumeValid(LoadableItemPermutationRotationIterator iterator) { + for (int i = 0; i < iterator.length(); i++) { + long calculatedMinStackableVolume = getMinStackableVolume(iterator, i); + long cachedMinStackableVolume = iterator.getMinStackableVolume(i); + + assertEquals(calculatedMinStackableVolume, cachedMinStackableVolume, "Calculated " + calculatedMinStackableVolume + ", got " + cachedMinStackableVolume); + } + } + + protected static long getMinStackableVolume(LoadableItemPermutationRotationIterator iterator, int offset) { + long minVolume = Long.MAX_VALUE; + for (int i = offset; i < iterator.length(); i++) { + StackValue stackValue = iterator.getStackValue(i); + long volume = stackValue.getVolume(); + if(volume < minVolume) { + minVolume = volume; + } + } + return minVolume; + } + + public abstract T newBuilder(); + + @Test + void testRotationCount() { + for (int i = 1; i <= 8; i++) { + Dimension container = new Dimension(null, 3 * (i + 1), 3, 1); + + List products1 = new ArrayList<>(); + + for (int k = 0; k < i; k++) { + Box box = Box.newBuilder().withSize(3, 1, 1).withRotate3D().withDescription(Integer.toString(k)).withWeight(1).build(); + + StackableItem item = new StackableItem(box); + + products1.add(item); + } + + LoadableItemPermutationRotationIterator rotator = + newBuilder() + .withLoadSize(container) + .withStackableItems(products1) + .withMaxLoadWeight(products1.size()) + .build(); + + long count = rotator.countRotations(); + + long rotate = 0; + do { + rotate++; + } while (rotator.nextRotation() != -1); + + assertEquals(count, rotate); + } + } + + @Test + void testUnconstrainedRotationCount() { + Dimension container = new Dimension(null, 3, 3, 3); + + List products = new ArrayList<>(); + + Box box = Box.newBuilder().withSize(1, 2, 3).withRotate3D().withDescription("0").withWeight(1).build(); + products.add(new StackableItem(box)); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + assertEquals(6, rotator.countRotations()); + } + + @Test + void testNumberOfConstrainedRotations() { + Dimension container = new Dimension(null, 1, 2, 3); + + List products = new ArrayList<>(); + + Box box = Box.newBuilder().withRotate3D().withSize(1, 2, 3).withDescription("0").withWeight(1).build(); + + products.add(new StackableItem(box)); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + assertEquals(1, rotator.countRotations()); + } + + @Test + void testNumberOfConstrainedRotationsWithOutOfScopeBox() { + Dimension container = new Dimension(null, 4, 4, 4); + + List products = new ArrayList<>(); + + Box box1 = Box.newBuilder().withRotate3D().withSize(1, 2, 3).withDescription("0").withWeight(1).build(); + Box box2 = Box.newBuilder().withRotate3D().withSize(5, 2, 2).withDescription("0").withWeight(1).build(); // too big + + products.add(new StackableItem(box1)); + products.add(new StackableItem(box2)); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + assertEquals(6, rotator.countRotations()); + } + + + @Test + void testNumberOfRotationsForSquare2D() { + Dimension container = new Dimension(null, 3, 3, 3); + + List products = new ArrayList<>(); + + Box box = Box.newBuilder().withSize(3, 1, 1).withRotate2D().withDescription("0").withWeight(1).build(); + products.add(new StackableItem(box)); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + assertEquals(2, rotator.countRotations()); + } + + @Test + void testNumberOfConstrainedRotationsForSquare2D() { + Dimension container = new Dimension(null, 3, 1, 1); + + List products = new ArrayList<>(); + + Box box = Box.newBuilder().withSize(3, 1, 1).withRotate2D().withDescription("0").withWeight(1).build(); + products.add(new StackableItem(box)); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + assertEquals(1, rotator.countRotations()); + } + + @Test + void testNumberOfRotationsForSquare3D() { + Dimension container = new Dimension(null, 3, 3, 3); + + List products = new ArrayList<>(); + + Box box = Box.newBuilder().withRotate3D().withSize(1, 1, 1).withDescription("0").withWeight(1).build(); + products.add(new StackableItem(box)); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + assertEquals(1, rotator.countRotations()); + } + + @Test + void testRotation() { + Dimension container = new Dimension(null, 9, 1, 1); + + List products = new ArrayList<>(); + + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("2").withWeight(1).build())); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + assertEquals(1, rotator.countRotations()); + + do { + // check order unchanged + for (int i = 0; i < products.size(); i++) { + assertEquals(Integer.toString(i), rotator.getStackValue(i).getStackable().getDescription()); + } + + // all rotations can fit + for (int i = 0; i < products.size(); i++) { + assertTrue(rotator.getStackValue(i).fitsInside3D(container)); + } + + assertMinStackableVolumeValid(rotator); + } while (rotator.nextRotation() != -1); + + } + + @Test + void testPermutations() { + Dimension container = new Dimension(null, 9, 1, 1); + + List products = new ArrayList<>(); + + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("2").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("3").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("4").withWeight(1).build())); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + int count = 0; + do { + assertMinStackableVolumeValid(rotator); + + count++; + } while (rotator.nextPermutation() != -1); + + assertEquals(5 * 4 * 3 * 2 * 1, count); + } + + @Test + void testPermutationsForMaxIndex() { + Dimension container = new Dimension(null, 9, 1, 1); + + List products = new ArrayList<>(); + + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("2").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("3").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("4").withWeight(1).build())); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + int count = 0; + do { + count++; + } while (rotator.nextPermutation(rotator.length() - 1) != -1); + + assertEquals(5 * 4 * 3 * 2 * 1, count); + } + + @Test + void testPermutationsForMaxIndexInRightOrder() { + Dimension container = new Dimension(null, 9, 1, 1); + + List products = new ArrayList<>(); + + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 4).withDescription("1").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 5).withDescription("2").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 6).withDescription("3").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 7).withDescription("4").withWeight(1).build())); + + LoadableItemPermutationRotationIterator rotator1 = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + LoadableItemPermutationRotationIterator rotator2 = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + int count = 0; + do { + assertMinStackableVolumeValid(rotator1); + assertMinStackableVolumeValid(rotator2); + + int[] permutations1 = rotator1.getPermutations(); + int[] permutations2 = rotator2.getPermutations(); + assertArrayEquals(permutations1, permutations2); + + count++; + } while (rotator1.nextPermutation(rotator1.length() - 1) != -1 && rotator2.nextPermutation() != -1); + + assertEquals(5 * 4 * 3 * 2 * 1, count); + } + + @Test + void testPermutationCorrectIndexReturned() { + Dimension container = new Dimension(null, 9, 1, 1); + + List products = new ArrayList<>(); + + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 4).withDescription("1").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 5).withDescription("2").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 6).withDescription("3").withWeight(1).build())); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + int count = 0; + do { + count++; + + int[] permutations = cloneArray(rotator.getPermutations()); + + int length = rotator.nextPermutation(); + + if(length == -1) { + break; + } + assertThat(firstDiffIndex(permutations, rotator.getPermutations())).isEqualTo(length); + + } while (true); + + assertEquals(4 * 3 * 2 * 1, count); + } + + public static int firstDiffIndex(int[] a, int[] b) { + for (int i = 0; i < a.length; i++) { + if(a[i] != b[i]) { + return i; + } + } + return -1; + } + + public static int[] cloneArray(int[] permutations) { + int[] clone = new int[permutations.length]; + System.arraycopy(permutations, 0, clone, 0, permutations.length); + return clone; + } + + @Test + void testPermutationsWithMultipleBoxes() { + Dimension container = new Dimension(null, 9, 1, 1); + + List products = new ArrayList<>(); + + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build(), 2)); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build(), 4)); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + int count = 0; + do { + count++; + } while (rotator.nextPermutation() != -1); + + assertEquals((6 * 5 * 4 * 3 * 2 * 1) / ((4 * 3 * 2 * 1) * (2 * 1)), count); + } + + @Test + void testCounts() { + List products = new ArrayList<>(); + + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(5, 10, 10).withDescription("0").withWeight(1).build(), 2)); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(5, 10, 10).withDescription("1").withWeight(1).build(), 2)); + + int n = 4; + + Dimension container = new Dimension(null, 5 * n, 10, 10); + + LoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + int length = rotator.length(); + + assertEquals(4, length); + + } + + @Test + void testCountPermutations1() { + int n = 25; + + Dimension container = new Dimension(null, 5 * n, 10, 10); + + List products = new ArrayList<>(); + for (int k = 0; k < n; k++) { + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(5, 10, 10).withWeight(1).build(), 1)); + } + + LoadableItemPermutationRotationIterator iterator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + assertEquals(-1L, iterator.countPermutations()); + } + + @Test + void testCountPermutations2() { + int n = 25; + + Dimension container = new Dimension(null, 5 * n, 10, 10); + + List products = new ArrayList<>(); + for (int k = 0; k < n; k++) { + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(5, 10, 10).withWeight(1).build(), 1)); + } + LoadableItemPermutationRotationIterator iterator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + assertEquals(-1L, iterator.countPermutations()); + } + + @Test + void testRemovePermutations1() { + Dimension container = new Dimension(null, 9, 1, 1); + + List products = new ArrayList<>(); + + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("2").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("3").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("4").withWeight(1).build())); + + LoadableItemPermutationRotationIterator iterator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + iterator.removePermutations(3); + + int[] permutations = iterator.getPermutations(); + + assertEquals(permutations.length, 2); + assertEquals(3, permutations[0]); + assertEquals(4, permutations[1]); + + int nextPermutation = iterator.nextPermutation(); + assertEquals(0, nextPermutation); + + // no more rotations + assertEquals(-1, iterator.nextPermutation()); + } + + @Test + void testRemovePermutations2() { + Dimension container = new Dimension(null, 9, 1, 1); + + List products = new ArrayList<>(); + + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("2").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("3").withWeight(1).build())); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("4").withWeight(1).build())); + + LoadableItemPermutationRotationIterator iterator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + List remove = new ArrayList<>(); + remove.add(2); + remove.add(4); + iterator.removePermutations(remove); + + int[] permutations = iterator.getPermutations(); + assertEquals(0, permutations[0]); + assertEquals(1, permutations[1]); + assertEquals(3, permutations[2]); + } + + +} diff --git a/core/src/test/java/com/github/skjolber/packing/iterator/DefaultLoadableItemGroupPermutationRotationIteratorTest.java b/core/src/test/java/com/github/skjolber/packing/iterator/DefaultLoadableItemGroupPermutationRotationIteratorTest.java new file mode 100644 index 00000000..bbc7b644 --- /dev/null +++ b/core/src/test/java/com/github/skjolber/packing/iterator/DefaultLoadableItemGroupPermutationRotationIteratorTest.java @@ -0,0 +1,345 @@ +package com.github.skjolber.packing.iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.github.skjolber.packing.api.Box; +import com.github.skjolber.packing.api.Dimension; +import com.github.skjolber.packing.api.StackableItem; +import com.github.skjolber.packing.api.StackableItemGroup; + +public class DefaultLoadableItemGroupPermutationRotationIteratorTest { + + @Test + void testPermutations() { + Dimension container = new Dimension(null, 9, 1, 1); + + List groups = new ArrayList<>(); + + StackableItemGroup group1 = new StackableItemGroup(); + List products1 = new ArrayList<>(); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("2").withWeight(1).build())); + group1.setItems(products1); + group1.setId("1"); + groups.add(group1); + + StackableItemGroup group2 = new StackableItemGroup(); + List products2 = new ArrayList<>(); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("3").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("4").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("5").withWeight(1).build())); + group2.setItems(products2); + group2.setId("2"); + + groups.add(group2); + + DefaultLoadableItemGroupPermutationRotationIterator rotator = DefaultLoadableItemGroupPermutationRotationIterator.newBuilder() + .withLoadSize(container) + .withStackableItemGroups(groups) + .withMaxLoadWeight(products2.size()) + .build(); + + int count = 0; + do { + count++; + System.out.println(Arrays.toString(rotator.getPermutations())); + } while (rotator.nextPermutation() != -1); + + assertEquals((3 * 2 * 1) * (3 * 2 * 1), count); + + assertEquals(count, rotator.countPermutations()); + } + + @Test + void testPermutationsRepeatedItems() { + Dimension container = new Dimension(null, 9, 1, 1); + + List groups = new ArrayList<>(); + + StackableItemGroup group1 = new StackableItemGroup(); + List products1 = new ArrayList<>(); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("2").withWeight(1).build())); + group1.setItems(products1); + group1.setId("1"); + groups.add(group1); + + + StackableItemGroup group2 = new StackableItemGroup(); + List products2 = new ArrayList<>(); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("3").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("4").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("5").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("6").withWeight(1).build(), 2)); + group2.setItems(products2); + group2.setId("2"); + + groups.add(group2); + + DefaultLoadableItemGroupPermutationRotationIterator rotator = DefaultLoadableItemGroupPermutationRotationIterator.newBuilder() + .withLoadSize(container) + .withStackableItemGroups(groups) + .withMaxLoadWeight(products2.size()) + .build(); + + int count = 0; + do { + count++; + System.out.println(Arrays.toString(rotator.getPermutations())); + } while (rotator.nextPermutation() != -1); + + assertEquals((3 * 2 * 1) * (5 * 4 * 3 * 2 * 1) / 2, count); + + assertEquals(count, rotator.countPermutations()); + } + + @Test + void testRemovePermutations() { + Dimension container = new Dimension(null, 9, 1, 1); + + List groups = new ArrayList<>(); + + StackableItemGroup group1 = new StackableItemGroup(); + List products1 = new ArrayList<>(); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("2").withWeight(1).build())); + group1.setItems(products1); + group1.setId("1"); + groups.add(group1); + + + StackableItemGroup group2 = new StackableItemGroup(); + List products2 = new ArrayList<>(); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("3").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("4").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("5").withWeight(1).build())); + group2.setItems(products2); + group2.setId("2"); + + groups.add(group2); + + DefaultLoadableItemGroupPermutationRotationIterator rotator = DefaultLoadableItemGroupPermutationRotationIterator.newBuilder() + .withLoadSize(container) + .withStackableItemGroups(groups) + .withMaxLoadWeight(products2.size()) + .build(); + + rotator.removePermutations(1); + + int count = 0; + do { + count++; + System.out.println(Arrays.toString(rotator.getPermutations())); + } while (rotator.nextPermutation() != -1); + + assertEquals((2 * 1) * (3 * 2 * 1), count); + + assertEquals(count, rotator.countPermutations()); + + // remove the rest of the group + rotator.removePermutations(2); + + assertEquals(3 * 2 * 1, rotator.countPermutations()); + } + + @Test + void testNexPermutationMaxIndexGroup1() { + Dimension container = new Dimension(null, 9, 1, 1); + + List groups = new ArrayList<>(); + + StackableItemGroup group1 = new StackableItemGroup(); + List products1 = new ArrayList<>(); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("2").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("3").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("4").withWeight(1).build())); + group1.setItems(products1); + group1.setId("1"); + groups.add(group1); + + + StackableItemGroup group2 = new StackableItemGroup(); + List products2 = new ArrayList<>(); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("5").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("6").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("7").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("8").withWeight(1).build())); + group2.setItems(products2); + group2.setId("2"); + + groups.add(group2); + + DefaultLoadableItemGroupPermutationRotationIterator iterator = DefaultLoadableItemGroupPermutationRotationIterator.newBuilder() + .withLoadSize(container) + .withStackableItemGroups(groups) + .withMaxLoadWeight(products2.size()) + .build(); + + int[] permutations = iterator.getPermutations(); + + int[] clone = new int[permutations.length]; + System.arraycopy(permutations, 0, clone, 0, clone.length); + + // forward to some random permutation within the group2 + for(int i = 0; i < 10; i++) { + iterator.nextPermutation(); + } + + int maxIndex = 3; + + iterator.nextPermutation(maxIndex); + + for(int i = 0; i < maxIndex; i++) { + assertEquals(clone[i], permutations[i]); + } + + assertNotEquals(clone[maxIndex], permutations[maxIndex]); + + // group 2 should be reset + for(int i = products1.size(); i < permutations.length; i++) { + assertEquals(clone[i], permutations[i]); + } + + + } + + + @Test + void testNexPermutationMaxIndexGroup2() { + Dimension container = new Dimension(null, 9, 1, 1); + + List groups = new ArrayList<>(); + + StackableItemGroup group1 = new StackableItemGroup(); + List products1 = new ArrayList<>(); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("2").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("3").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("4").withWeight(1).build())); + group1.setItems(products1); + group1.setId("1"); + groups.add(group1); + + + StackableItemGroup group2 = new StackableItemGroup(); + List products2 = new ArrayList<>(); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("5").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("6").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("7").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("8").withWeight(1).build())); + group2.setItems(products2); + group2.setId("2"); + + groups.add(group2); + + DefaultLoadableItemGroupPermutationRotationIterator iterator = DefaultLoadableItemGroupPermutationRotationIterator.newBuilder() + .withLoadSize(container) + .withStackableItemGroups(groups) + .withMaxLoadWeight(products2.size()) + .build(); + + int[] permutations = iterator.getPermutations(); + + int[] clone = new int[permutations.length]; + System.arraycopy(permutations, 0, clone, 0, clone.length); + + System.out.println(Arrays.toString(iterator.getPermutations())); + + int maxIndex = 6; + + iterator.nextPermutation(maxIndex); + + System.out.println(Arrays.toString(iterator.getPermutations())); + + for(int i = 0; i < maxIndex; i++) { + assertEquals(clone[i], permutations[i]); + } + + assertNotEquals(clone[maxIndex], permutations[maxIndex]); + + // group 1 should not be touched + for(int i = 0; i < products1.size(); i++) { + assertEquals(clone[i], permutations[i]); + } + + + } + + + + @Test + void testNexPermutationMaxIndexTransitionGroup() { + Dimension container = new Dimension(null, 9, 1, 1); + + List groups = new ArrayList<>(); + + StackableItemGroup group1 = new StackableItemGroup(); + List products1 = new ArrayList<>(); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("0").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("1").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("2").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("3").withWeight(1).build())); + products1.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("4").withWeight(1).build())); + group1.setItems(products1); + group1.setId("1"); + groups.add(group1); + + + StackableItemGroup group2 = new StackableItemGroup(); + List products2 = new ArrayList<>(); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("5").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("6").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("7").withWeight(1).build())); + products2.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withDescription("8").withWeight(1).build())); + group2.setItems(products2); + group2.setId("2"); + + groups.add(group2); + + DefaultLoadableItemGroupPermutationRotationIterator iterator = DefaultLoadableItemGroupPermutationRotationIterator.newBuilder() + .withLoadSize(container) + .withStackableItemGroups(groups) + .withMaxLoadWeight(products2.size()) + .build(); + + // go to the last permuation of the second group + for(int i = 0; i < 4 * 3 * 2 * 1 - 1; i++) { + iterator.nextPermutation(); + } + + int[] permutations = iterator.getPermutations(); + + int[] clone = new int[permutations.length]; + System.arraycopy(permutations, 0, clone, 0, clone.length); + + System.out.println(Arrays.toString(iterator.getPermutations())); + + int maxIndex = 6; + + int index = iterator.nextPermutation(maxIndex); + + assertTrue(index < products1.size()); + + System.out.println(Arrays.toString(iterator.getPermutations())); + + for(int i = 0; i < index; i++) { + assertEquals(clone[i], permutations[i]); + } + + assertNotEquals(clone[index], permutations[index]); + } +} diff --git a/core/src/test/java/com/github/skjolber/packing/iterator/LoadableItemPermutationRotationIteratorTest.java b/core/src/test/java/com/github/skjolber/packing/iterator/LoadableItemPermutationRotationIteratorTest.java new file mode 100644 index 00000000..3b6fb8a9 --- /dev/null +++ b/core/src/test/java/com/github/skjolber/packing/iterator/LoadableItemPermutationRotationIteratorTest.java @@ -0,0 +1,12 @@ +package com.github.skjolber.packing.iterator; + +import com.github.skjolber.packing.iterator.DefaultLoadableItemPermutationRotationIterator.Builder; + +class LoadableItemPermutationRotationIteratorTest extends AbstractLoadablePermutationRotationIteratorTest { + + @Override + public Builder newBuilder() { + return DefaultLoadableItemPermutationRotationIterator.newBuilder(); + } + +} diff --git a/core/src/test/java/com/github/skjolber/packing/iterator/MutableLoadablePermutationRotationIteratorTest.java b/core/src/test/java/com/github/skjolber/packing/iterator/MutableLoadablePermutationRotationIteratorTest.java new file mode 100644 index 00000000..e68500fc --- /dev/null +++ b/core/src/test/java/com/github/skjolber/packing/iterator/MutableLoadablePermutationRotationIteratorTest.java @@ -0,0 +1,167 @@ +package com.github.skjolber.packing.iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.github.skjolber.packing.api.Box; +import com.github.skjolber.packing.api.Dimension; +import com.github.skjolber.packing.api.StackableItem; +import com.github.skjolber.packing.api.packager.LoadableItem; +import com.github.skjolber.packing.api.packager.LoadableItems; + +class MutableLoadablePermutationRotationIteratorTest extends AbstractLoadablePermutationRotationIteratorTest { + + @Override + public MutableLoadableItemPermutationRotationIterator.Builder newBuilder() { + return MutableLoadableItemPermutationRotationIterator.newMutableBuilder(); + } + + @Test + void testMutableRotationCount() { + for (int i = 1; i <= 8; i++) { + Dimension container = new Dimension(null, 3 * (i + 1), 3, 1); + + List products1 = new ArrayList<>(); + + for (int k = 0; k < i; k++) { + Box box = Box.newBuilder().withSize(3, 1, 1).withRotate3D().withId(Integer.toString(k)).withWeight(1).build(); + + StackableItem item = new StackableItem(box); + + products1.add(item); + } + + MutableLoadableItemPermutationRotationIterator rotator = + newBuilder() + .withLoadSize(container) + .withStackableItems(products1) + .withMaxLoadWeight(products1.size()) + .build(); + + LoadableItems items = rotator; + + long unmodifiedRotationsCount = rotator.countRotations(); + + long modifiedRotationsCount = rotator.countMutableRotations(); + + assertTrue(unmodifiedRotationsCount >= modifiedRotationsCount); + + long rotate = 0; + do { + // removing items do not affect the number of rotations + assertEquals(items.size(), products1.size()); + + items.remove(0, 1); + for(int k = 0; k < items.size(); k++) { + LoadableItem loadableItem = items.get(k); + assertFalse(loadableItem.getLoadable().getStackable().getId().equals("0")); + } + + rotate++; + } while (rotator.nextRotation() != -1); + + assertEquals(unmodifiedRotationsCount, rotate); + + System.out.println(unmodifiedRotationsCount + " -> " + modifiedRotationsCount); + } + } + + @Test + void testMutablePermutationsWithMultipleBoxes() { + Dimension container = new Dimension(null, 9, 1, 1); + + List products = new ArrayList<>(); + + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withId("0").withWeight(1).build(), 2)); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withId("1").withWeight(1).build(), 4)); + + MutableLoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + + int count = 0; + do { + assertEquals(rotator.size(), products.size()); + + // removing items do not affect the number of permutations + rotator.remove(0, 1); + + // still two types of loadable items + assertEquals(rotator.size(), 2); + + count++; + } while (rotator.nextPermutation() != -1); + + assertEquals((6 * 5 * 4 * 3 * 2 * 1) / ((4 * 3 * 2 * 1) * (2 * 1)), count); + } + + + @Test + void testLoadableItems() { + Dimension container = new Dimension(null, 9, 1, 1); + + List products = new ArrayList<>(); + + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withId("0").withWeight(1).build(), 2)); + products.add(new StackableItem(Box.newBuilder().withRotate3D().withSize(1, 1, 3).withId("1").withWeight(1).build(), 4)); + + MutableLoadableItemPermutationRotationIterator rotator = newBuilder() + .withLoadSize(container) + .withStackableItems(products) + .withMaxLoadWeight(products.size()) + .build(); + + // removing one item do not affect the number of permutations + rotator.remove(0, 1); + + // still two types of loadable items + assertEquals(rotator.size(), 2); + + int[] frequencies = toFrequency(rotator, 2); + + assertEquals(1, frequencies[0]); + assertEquals(4, frequencies[1]); + + // still two types of loadable items + rotator.remove(1, 2); + assertEquals(rotator.size(), 2); + + frequencies = toFrequency(rotator, 2); + assertEquals(1, frequencies[0]); + assertEquals(2, frequencies[1]); + + rotator.remove(0, 1); + // 0 exhausted + assertEquals(rotator.size(), 1); + + frequencies = toFrequency(rotator, 2); + assertEquals(0, frequencies[0]); + assertEquals(2, frequencies[1]); + + rotator.remove(0, 2); + // 1 exhausted + assertEquals(rotator.size(), 0); + } + + public int[] toFrequency(MutableLoadableItemPermutationRotationIterator rotator, int size) { + int[] counts = new int[size]; + for (int i : rotator.getPermutations()) { + counts[i]++; + } + return counts; + } + + + + +}