diff --git a/primitiveCollections/src/main/java/org/lucares/collections/BiLongObjectFunction.java b/primitiveCollections/src/main/java/org/lucares/collections/BiLongObjectFunction.java new file mode 100644 index 0000000..175e627 --- /dev/null +++ b/primitiveCollections/src/main/java/org/lucares/collections/BiLongObjectFunction.java @@ -0,0 +1,5 @@ +package org.lucares.collections; + +public interface BiLongObjectFunction { + O apply(long key, O value); +} diff --git a/primitiveCollections/src/main/java/org/lucares/collections/LongObjConsumer.java b/primitiveCollections/src/main/java/org/lucares/collections/LongObjConsumer.java new file mode 100644 index 0000000..81e0816 --- /dev/null +++ b/primitiveCollections/src/main/java/org/lucares/collections/LongObjConsumer.java @@ -0,0 +1,5 @@ +package org.lucares.collections; + +public interface LongObjConsumer { + void accept(long key, O value); +} diff --git a/primitiveCollections/src/main/java/org/lucares/collections/LongObjHashMap.java b/primitiveCollections/src/main/java/org/lucares/collections/LongObjHashMap.java new file mode 100644 index 0000000..e42bc87 --- /dev/null +++ b/primitiveCollections/src/main/java/org/lucares/collections/LongObjHashMap.java @@ -0,0 +1,354 @@ +package org.lucares.collections; + +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.function.Supplier; + +/** + * A hash map where the key is a long and the value is a generic object. + * + * @see LongLongHashMap + */ +public class LongObjHashMap { + + // There is no equivalent to null for primitive values. Therefore we have to add + // special handling for one long value. Otherwise we couldn't tell if a key is + // in the map or not. We chose 0L, because LongList is initially all 0L. + private static final long NULL_KEY = 0L; + + private static final long EMPTY_SLOT = 0L; + + /** + * The maximum size of an array. + */ + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + + private final double fillFactor; + + private long[] keys; + private V[] values; + private int size = 0; + + private V zeroValue = null; + + /** + * Create a new {@link LongLongHashMap} with the given initial capacity and load + * factor. + * + * @param initialCapacity the initial capacity + * @param loadFactor the load factor + */ + @SuppressWarnings("unchecked") + public LongObjHashMap(final int initialCapacity, final double loadFactor) { + + if (initialCapacity < 0) { + throw new IllegalArgumentException("initial capacity must be non-negative"); + } + if (initialCapacity > MAX_ARRAY_SIZE) { + throw new IllegalArgumentException("initial capacity must be smaller or equal to " + MAX_ARRAY_SIZE); + } + if (loadFactor <= 0 || Double.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + loadFactor); + + this.fillFactor = loadFactor; + keys = new long[initialCapacity]; + values = (V[]) new Object[initialCapacity]; + } + + /** + * Create a new {@link LongLongHashMap} with initial capacity 8 and load factor + * 0.75. + */ + public LongObjHashMap() { + this(8, 0.75); + } + + /** + * The number of entries in this map. + * + * @return the size + */ + public int size() { + return size; + } + + /** + * The capacity of this map. + * + * @return the capacity + */ + int getCapacity() { + return keys.length; + } + + /** + * Add the given key and value to the map. + * + * @param key the key + * @param value the value + */ + public void put(final long key, final V value) { + + if (key == NULL_KEY) { + size += zeroValue == null ? 1 : 0; + zeroValue = value; + return; + } + + if ((keys.length * fillFactor) < size) { + growAndRehash(); + } + + final boolean added = putInternal(key, value); + if (added) { + size++; + } + } + + private boolean putInternal(final long key, final V value) { + final int searchStart = spread(key); + int currentPosition = searchStart; + + do { + // found a free place, insert the value + if (keys[currentPosition] == EMPTY_SLOT) { + keys[currentPosition] = key; + values[currentPosition] = value; + return true; + } + // value exists, update it + if (keys[currentPosition] == key) { + keys[currentPosition] = key; + values[currentPosition] = value; + return false; + } + currentPosition = (currentPosition + 1) % keys.length; + } while (currentPosition != searchStart); + + throw new IllegalStateException("map is full"); + } + + /** + * Returns the value for the given key if it exists. This method throws a + * {@link NoSuchElementException} if the key does not exist. Use + * {@link #containsKey(long)} to check before calling {@link #get(long)}. + * + * @param key the key + * @return the value if it exists, or {@code null} if the value does not exist + */ + public V get(final long key) { + + if (key == NULL_KEY) { + if (zeroValue != null) { + return zeroValue; + } + return null; + } + + final int searchStart = spread(key); + int currentPosition = searchStart; + do { + if (keys[currentPosition] == key) { + return values[currentPosition]; + } + currentPosition = (currentPosition + 1) % keys.length; + } while (currentPosition != searchStart); + return null; + } + + /** + * Check if the map contains the given key. + * + * @param key the key + * @return true iff the map contains the key + */ + public boolean containsKey(final long key) { + + if (key == NULL_KEY) { + return zeroValue != null; + } + + final int searchStart = spread(key); + int currentPosition = searchStart; + do { + if (keys[currentPosition] == key) { + return true; + } + currentPosition = (currentPosition + 1) % keys.length; + } while (currentPosition != searchStart); + return false; + } + + /** + * Remove the given key and its value from the map. + * + * @param key the key + */ + public void remove(final long key) { + + if (key == NULL_KEY) { + size -= zeroValue != null ? 1 : 0; + zeroValue = null; + return; + } + + final int searchStart = spread(key); + int currentPosition = searchStart; + do { + if (keys[currentPosition] == key) { + keys[currentPosition] = EMPTY_SLOT; + size--; + return; + } + currentPosition = (currentPosition + 1) % keys.length; + } while (currentPosition != searchStart); + } + + /** + * Computes a mapping for the given key and its current value. + *

+ * The mapping for given key is updated by calling {@code function} with the old + * value. The return value will be set as new value. If the map does not contain + * a mapping for the key, then {@code function} is called with + * {@code initialValueIfAbsent}. + * + * @param key the key + * @param initialValueIfAbsent a {@link Supplier} returning the value used if there is no current mapping for the + * key + * @param function called to update an existing value + */ + public void compute(final long key, final Supplier initialValueIfAbsent, final BiLongObjectFunction function) { + if (key == NULL_KEY) { + if (zeroValue != null) { + zeroValue = function.apply(NULL_KEY, zeroValue); + return; + } + zeroValue = function.apply(NULL_KEY, initialValueIfAbsent.get()); + return; + } + + final int searchStart = spread(key); + int currentPosition = searchStart; + do { + if (keys[currentPosition] == key) { + final V updatedValue = function.apply(key, values[currentPosition]); + values[currentPosition] = updatedValue; + return; + } + currentPosition = (currentPosition + 1) % keys.length; + } while (currentPosition != searchStart); + + // key not found -> add it + final V newZeroValue = function.apply(key, initialValueIfAbsent.get()); + put(key, newZeroValue); + } + + /** + * Calls the {@link LongObjConsumer#accept(long, Object)} method for all entries + * in this map. The order is based on the hash value and is therefore not + * deterministic. Don't rely on the order! + * + * @param consumer the consumer + */ + public void forEach(final LongObjConsumer consumer) { + + if (zeroValue != null) { + consumer.accept(0, zeroValue); + } + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != EMPTY_SLOT) { + consumer.accept(keys[i], values[i]); + } + } + } + + /** + * Calls the {@link LongObjConsumer#accept(long, Object)} method for all entries + * in this map. This method iterates over the keys in ascending order. + *

+ * Note: this method is slower than {@link #forEach(LongLongConsumer)}. + * + * @param consumer the consumer + */ + public void forEachOrdered(final LongObjConsumer consumer) { + + if (zeroValue != null) { + consumer.accept(0, zeroValue); + } + + final long[] sortedKeys = Arrays.copyOf(keys, keys.length); + Arrays.parallelSort(sortedKeys); + + for (int i = 0; i < sortedKeys.length; i++) { + final long key = sortedKeys[i]; + if (key != EMPTY_SLOT) { + consumer.accept(key, get(key)); + } else if (key == EMPTY_SLOT) { + final int posFirstKey = findPosOfFirstPositiveKey(sortedKeys); + if (posFirstKey < 0) { + return; + } + i = posFirstKey - 1; + } + } + } + + static int findPosOfFirstPositiveKey(final long[] sortedKeys) { + + if (sortedKeys.length == 0) { + return -1; + } + if (sortedKeys.length == 1) { + return sortedKeys[0] > EMPTY_SLOT ? 0 : -1; + } + + int low = 0; + int high = sortedKeys.length - 1; + int pos = -1; + + while (low <= high) { + pos = (low + high) / 2; + if (sortedKeys[pos] <= EMPTY_SLOT) { + low = pos + 1; + } else { + high = pos - 1; + } + } + + if (low < sortedKeys.length && sortedKeys[low] <= EMPTY_SLOT) { + low++; + } + + return low < sortedKeys.length && sortedKeys[low] > EMPTY_SLOT ? low : -1; + } + + @SuppressWarnings("unchecked") + private void growAndRehash() { + final long[] oldKeys = keys; + final V[] oldValues = values; + + final int newSize = Math.min(keys.length * 2, MAX_ARRAY_SIZE); + + keys = new long[newSize]; + values = (V[]) new Object[newSize]; + + for (int i = 0; i < oldKeys.length; i++) { + final long key = oldKeys[i]; + if (key != EMPTY_SLOT) { + final V value = oldValues[i]; + putInternal(key, value); + } + } + } + + // visible for test + int spread(final long key) { + return hash(key) % keys.length; + } + + private int hash(final long l) { + return Math.abs(Long.hashCode(l)); + } + +} diff --git a/primitiveCollections/src/main/java/org/lucares/collections/Sparse2DLongArray.java b/primitiveCollections/src/main/java/org/lucares/collections/Sparse2DLongArray.java new file mode 100644 index 0000000..3561edc --- /dev/null +++ b/primitiveCollections/src/main/java/org/lucares/collections/Sparse2DLongArray.java @@ -0,0 +1,202 @@ +package org.lucares.collections; + +import java.util.function.Supplier; + +/** + * A sparse 2-dimensional array of primitive longs. + */ +public class Sparse2DLongArray { + + public static final int MAX_SIZE = -1; + + private static final class MinMaxValue implements TripleLongConsumer { + + long minIndex1 = Long.MAX_VALUE; + long maxIndex1 = Long.MIN_VALUE; + + long minIndex2 = Long.MAX_VALUE; + long maxIndex2 = Long.MIN_VALUE; + + long minValue = Long.MAX_VALUE; + long maxValue = Long.MIN_VALUE; + + @Override + public void apply(long index1, long index2, long value) { + + minIndex1 = Math.min(minIndex1, index1); + maxIndex1 = Math.max(maxIndex1, index1); + + minIndex2 = Math.min(minIndex2, index2); + maxIndex2 = Math.max(maxIndex2, index2); + + minValue = Math.min(minValue, value); + maxValue = Math.max(maxValue, value); + } + + public long getMaxIndex1() { + return maxIndex1; + } + + public long getMaxIndex2() { + return maxIndex2; + } + + public long getMaxValue() { + return maxValue; + } + + public long getMinValue() { + return minValue; + } + } + + private final LongObjHashMap matrix; + private final Supplier initialValueSupplier; + private long sizeX; + private long sizeY; + + /** + * Create a new {@link Sparse2DLongArray} with arbitrary size. You can use all + * values in the interval 0 (inclusive) to {@link Long#MAX_VALUE} (inclusive) as + * index for the x/y axis. + */ + public Sparse2DLongArray() { + this(MAX_SIZE, MAX_SIZE, 8, 0.75); + } + + /** + * Create a new {@link Sparse2DLongArray} with specified size. + * + * @param sizeX size of the x-axis, use {@link Sparse2DLongArray#MAX_SIZE} if + * you want to be able to use {@link Long#MAX_VALUE} as index for + * the x-axis + * @param sizeY size of the y-axis, use {@link Sparse2DLongArray#MAX_SIZE} if + * you want to be able to use {@link Long#MAX_VALUE} as index for + * the y-axis + */ + public Sparse2DLongArray(long sizeX, long sizeY) { + this(sizeX, sizeY, 8, 0.75); + } + + /** + * Create a new {@link Sparse2DLongArray} with the given initial capacity and + * load factor. + * + * @param sizeX size of the x-axis, use + * {@link Sparse2DLongArray#MAX_SIZE} if you want to be + * able to use {@link Long#MAX_VALUE} as index for the + * x-axis + * @param sizeY size of the y-axis, use + * {@link Sparse2DLongArray#MAX_SIZE} if you want to be + * able to use {@link Long#MAX_VALUE} as index for the + * y-axis + * @param initialCapacity the initial capacity + * @param loadFactor the load factor + */ + public Sparse2DLongArray(long sizeX, long sizeY, int initialCapacity, double loadFactor) { + if (sizeX <= 0 && sizeX != MAX_SIZE || sizeY <= 0 & sizeY != MAX_SIZE) { + throw new IllegalArgumentException("size in x and y axis must be positive"); + } + + this.sizeX = sizeX; + this.sizeY = sizeY; + this.matrix = new LongObjHashMap(initialCapacity, loadFactor); + initialValueSupplier = () -> new LongLongHashMap(initialCapacity, loadFactor); + } + + /** + * Add or overwrite a value at position {@code x}×{@code y} in the array. + * + * @param x index of the first dimension, must not be negative + * @param y index of the second dimension, must not be negative + * @param value the value + */ + public void put(long x, long y, long value) { + + if (x < 0 || y < 0) { + throw new IllegalArgumentException("indexes must be non-negative"); + } + if (sizeX != MAX_SIZE && x >= sizeX) { + throw new IndexOutOfBoundsException("x-axis out of range: " + x); + } + if (sizeY != MAX_SIZE && y >= sizeY) { + throw new IndexOutOfBoundsException("y-axis out of range: " + y); + } + matrix.compute(x, initialValueSupplier, (key, oldValue) -> { + oldValue.put(y, value); + return oldValue; + }); + } + + /** + * Get value from position {@code x}×{@code y} of the array + * + * @param x index of the first dimension, must not be negative + * @param y index of the second dimension, must not be negative + * @return the value, or 0 if no value exists + */ + public long get(long x, long y) { + if (x < 0 || y < 0) { + throw new IllegalArgumentException("indexes must be non-negative"); + } + + LongLongHashMap longLongHashMap = matrix.get(x); + if (longLongHashMap != null) { + return longLongHashMap.get(y, 0); + } + return 0; + } + + /** + * Calls the {@link TripleLongConsumer#apply(long, long, long)} method for all + * entries in this map. The order is not deterministic. Don't rely on the order! + * + * @param consumer the consumer + */ + public void forEach(TripleLongConsumer consumer) { + matrix.forEach((x, yDimension) -> { + yDimension.forEach((y, value) -> consumer.apply(x, y, value)); + }); + } + + @Override + public String toString() { + return toString(120); + } + + /** + * Prints (parts) of the matrix as 2-dimensional table with + * {@code maxWidthInCharacters} + * + * @param maxWidthInCharacters maximum number of characters (horizontally) + * @return the matrix + */ + public String toString(long maxWidthInCharacters) { + + final MinMaxValue minMaxValue = new MinMaxValue(); + forEach(minMaxValue); + + final long maxIndex1 = minMaxValue.getMaxIndex1(); + + final long maxIndex2 = minMaxValue.getMaxIndex2(); + + final long lengthOfMaxValue = Math.max(Long.toString(minMaxValue.getMaxValue()).length(), + Long.toString(minMaxValue.getMinValue()).length()); + + final StringBuilder s = new StringBuilder(); + if (maxIndex2 < maxWidthInCharacters / (lengthOfMaxValue + 1)) { + String format = "%" + (lengthOfMaxValue + 1) + "d"; + + for (int i = 0; i <= maxIndex1; i++) { + for (int j = 0; j <= maxIndex2; j++) { + s.append(String.format(format, get(i, j))); + } + s.append("\n"); + } + return s.toString(); + } else { + forEach((index1, index2, value) -> s.append(String.format("(%d, %d) = %d\n", index1, index2, value))); + } + return s.toString(); + } +} diff --git a/primitiveCollections/src/main/java/org/lucares/collections/TripleLongConsumer.java b/primitiveCollections/src/main/java/org/lucares/collections/TripleLongConsumer.java new file mode 100644 index 0000000..ded519b --- /dev/null +++ b/primitiveCollections/src/main/java/org/lucares/collections/TripleLongConsumer.java @@ -0,0 +1,15 @@ +package org.lucares.collections; + +/** + * Consumer for iterating over {@link Sparse2DLongArray}s. + */ +public interface TripleLongConsumer { + + /** + * Performs this operation on the given arguments + * @param x the index in the x-dimension + * @param y the index in the y-dimension + * @param value the value at {@code x}×{@code y} + */ + public void apply(long x, long y, long value); +} diff --git a/primitiveCollections/src/test/java/org/lucares/collections/LongObjHashMapTest.java b/primitiveCollections/src/test/java/org/lucares/collections/LongObjHashMapTest.java new file mode 100644 index 0000000..f38e7a8 --- /dev/null +++ b/primitiveCollections/src/test/java/org/lucares/collections/LongObjHashMapTest.java @@ -0,0 +1,250 @@ +package org.lucares.collections; + +import java.util.Random; +import java.util.function.Supplier; +import java.util.stream.LongStream; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class LongObjHashMapTest { + + private static final class LongSupplier implements Supplier{ + + private final Long value ; + + public LongSupplier(Long value) { + this.value = value; + } + + @Override + public Long get() { + return value; + } + } + + @Test + public void testPutRemove() { + putGetRemove(1); + } + + @Test + public void testNullValue() { + putGetRemove(0); + } + + private void putGetRemove(final long key) { + final LongObjHashMap map = new LongObjHashMap<>(); + + final long valueA = 2L; + final long valueB = 3L; + + // value does not exist + Assertions.assertFalse(map.containsKey(key)); + Assertions.assertEquals(0, map.size()); + + // add value and check it is in the map + map.put(key, valueA); + Assertions.assertTrue(map.containsKey(key)); + Assertions.assertEquals(valueA, map.get(key)); + Assertions.assertEquals(1, map.size()); + + // overwrite value + map.put(key, valueB); + Assertions.assertEquals(valueB, map.get(key)); + Assertions.assertEquals(1, map.size()); + + // remove value and check it is gone + map.remove(key); + Assertions.assertFalse(map.containsKey(key)); + Assertions.assertEquals(0, map.size()); + } + + @Test + public void testComputeZeroKey() { + final LongObjHashMap map = new LongObjHashMap<>(); + + final long key = 1; + map.compute(key, new LongSupplier(6L), (k,l) -> l + 1); + Assertions.assertEquals(7, map.get(key), "initialValueIfAbsent is used when there is no mapping for the key"); + + map.compute(key, new LongSupplier(6L), (k,l) -> l + 1); + Assertions.assertEquals(8, map.get(key), "update function is called when 'zeroKey' is set"); + } + + @Test + public void testCompute() { + final LongObjHashMap map = new LongObjHashMap<>(); + final long key = 1; + map.compute(key, new LongSupplier(6L), (k,l) -> l + 1); + Assertions.assertEquals(7, map.get(key), "initialValueIfAbsent is used when there is no mapping for the key"); + + map.compute(key, new LongSupplier(6L), (k,l) -> l + 1); + Assertions.assertEquals(8, map.get(key), "update function is called when key is set"); + } + + @Test + public void testGrowMap() { + final LongObjHashMap map = new LongObjHashMap<>(4, 0.75); + + final int numEntries = 12; + final Random rand = new Random(12345); + final LongList entries = LongList.of(LongStream.generate(rand::nextLong).limit(numEntries).toArray()); + + entries.stream().forEachOrdered(l -> { + map.put(l, l); + }); + entries.stream().forEachOrdered(l -> { + Assertions.assertEquals(l, map.get(l)); + }); + Assertions.assertEquals(16, map.getCapacity(), "capacity after adding 12 entries must be a the smallest number " + + "that satisfies initialCapacity * 2^n >= entries/fillFactor"); + } + + @Test + public void testMultipleValuesOnSamePosition() { + final int initialCapacity = 20; + final LongObjHashMap map = new LongObjHashMap<>(initialCapacity, 0.75); + // find to values that yield the same 'spread' (position in the table) + final LongList keysWithSameSpread = findKeysWithSameSpread(map); + Assertions.assertTrue(keysWithSameSpread.size() > 5); + + keysWithSameSpread.stream().forEach(l -> map.put(l, l)); + Assertions.assertEquals(keysWithSameSpread.size(), map.size()); + keysWithSameSpread.stream().forEach(l -> Assertions.assertEquals(l, map.get(l))); + } + + @Test + public void testForEach() { + final LongObjHashMap map = new LongObjHashMap<>(); + final Random rand = new Random(6789); + final LongList entries = LongList.of(LongStream.generate(rand::nextLong).limit(15).toArray()); + + entries.stream().forEachOrdered(l -> { + map.put(l, 2 * l); + }); + + map.forEach((k, v) -> { + Assertions.assertEquals(k * 2, v, "value is key*2"); + Assertions.assertTrue(entries.indexOf(k) >= 0, "value " + k + " in entries: " + entries); + }); + } + + @Test + public void testForEachOrdered() { + final LongObjHashMap map = new LongObjHashMap<>(); + final Random rand = new Random(6789); + final LongList entries = LongList.of(LongStream.generate(rand::nextLong).limit(15).toArray()); + + entries.stream().forEachOrdered(l -> { + map.put(l, 2 * l); + }); + + final LongList actualOrderOfKeys = new LongList(); + map.forEachOrdered((k, v) -> { + Assertions.assertEquals(k * 2, v, "value is key*2"); + Assertions.assertTrue(entries.indexOf(k) >= 0, "value " + k + " in entries: " + entries); + actualOrderOfKeys.add(k); + }); + + Assertions.assertTrue(actualOrderOfKeys.isSorted(), "keys are sorted"); + Assertions.assertEquals(LongList.intersection(actualOrderOfKeys, entries).size(), entries.size(), + "all keys were visited"); + final LongList additionalKeys = new LongList(actualOrderOfKeys); + additionalKeys.removeAll(entries); + Assertions.assertEquals(additionalKeys, LongList.of(), "no additional keys were visited"); + } + + @Test + public void testForEachOrderedOnlyNegativeValues() { + final LongObjHashMap map = new LongObjHashMap<>(); + final LongList entries = LongList.of(LongStream.range(-20, -5).toArray()); + + entries.stream().forEachOrdered(l -> { + map.put(l, 2 * l); + }); + + final LongList actualOrderOfKeys = new LongList(); + map.forEachOrdered((k, v) -> { + Assertions.assertEquals(k * 2, v, "value is key*2"); + Assertions.assertTrue(entries.indexOf(k) >= 0, "value " + k + " in entries: " + entries); + actualOrderOfKeys.add(k); + }); + + Assertions.assertTrue(actualOrderOfKeys.isSorted(), "keys are sorted"); + Assertions.assertEquals(LongList.intersection(actualOrderOfKeys, entries).size(), entries.size(), + "all keys were visited"); + final LongList additionalKeys = new LongList(actualOrderOfKeys); + additionalKeys.removeAll(entries); + Assertions.assertEquals(additionalKeys, LongList.of(), "no additional keys were visited"); + } + + @Test + public void testForEachOrderedOnlyNegativeValues2() { + final LongObjHashMap map = new LongObjHashMap<>(); + final LongList entries = LongList.of(LongStream.range(-20, -5).toArray()); + + entries.stream().forEachOrdered(l -> { + map.put(l, 2 * l); + }); + + final LongList actualOrderOfKeys = new LongList(); + map.forEachOrdered((k, v) -> { + Assertions.assertEquals(k * 2, v, "value is key*2"); + Assertions.assertTrue(entries.indexOf(k) >= 0, "value " + k + " in entries: " + entries); + actualOrderOfKeys.add(k); + }); + + Assertions.assertTrue(actualOrderOfKeys.isSorted(), "keys are sorted"); + } + + @Test + public void testFindPositionOfFirstPositiveKey() { + + Assertions.assertEquals(-1, LongObjHashMap.findPosOfFirstPositiveKey(new long[] {})); + Assertions.assertEquals(-1, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 0 })); + Assertions.assertEquals(0, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 1 })); + Assertions.assertEquals(1, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 0, 1 })); + Assertions.assertEquals(0, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 1, 1 })); + Assertions.assertEquals(2, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { -1, 0, 1 })); + Assertions.assertEquals(0, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 1, 1, 1, 1, 1, 1, 1, 1, 1 })); + Assertions.assertEquals(0, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 1, 1, 1, 1, 1, 1, 1, 1 })); + Assertions.assertEquals(4, + LongObjHashMap.findPosOfFirstPositiveKey(new long[] { -1, -1, -1, -1, 1, 1, 1, 1, 1 })); + Assertions.assertEquals(4, + LongObjHashMap.findPosOfFirstPositiveKey(new long[] { -1, -1, -1, -1, 1, 1, 1, 1 })); + Assertions.assertEquals(3, + LongObjHashMap.findPosOfFirstPositiveKey(new long[] { -1, -1, -1, 1, 1, 1, 1, 1, 1 })); + Assertions.assertEquals(3, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { -1, -1, -1, 1, 1, 1, 1, 1 })); + Assertions.assertEquals(-1, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 0, 0, 0, 0, 0 })); + Assertions.assertEquals(-1, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 0, 0, 0, 0, 0, 0 })); + Assertions.assertEquals(4, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 0, 0, 0, 0, 1, 1, 1, 1 })); + Assertions.assertEquals(5, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 0, 0, 0, 0, 0, 1, 1, 1 })); + Assertions.assertEquals(6, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 0, 0, 0, 0, 0, 0, 1, 1 })); + Assertions.assertEquals(4, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 0, 0, 0, 0, 1, 1, 1 })); + Assertions.assertEquals(5, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 0, 0, 0, 0, 0, 1, 1 })); + Assertions.assertEquals(6, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { 0, 0, 0, 0, 0, 0, 1 })); + Assertions.assertEquals(4, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { -1, 0, 0, 0, 1, 1, 1 })); + Assertions.assertEquals(5, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { -1, 0, 0, 0, 0, 1, 1 })); + Assertions.assertEquals(6, LongObjHashMap.findPosOfFirstPositiveKey(new long[] { -1, 0, 0, 0, 0, 0, 1 })); + } + + + + private LongList findKeysWithSameSpread(final LongObjHashMap map) { + final LongList result = new LongList(); + final int spread = map.spread(1); + result.add(1); + for (long l = 2; l < 10000; l++) { + final int s = map.spread(l); + if (s == spread) { + result.add(l); + if (result.size() > 10) { + break; + } + } + } + + return result; + } +} diff --git a/primitiveCollections/src/test/java/org/lucares/collections/Sparse2DLongArrayTest.java b/primitiveCollections/src/test/java/org/lucares/collections/Sparse2DLongArrayTest.java new file mode 100644 index 0000000..c635e73 --- /dev/null +++ b/primitiveCollections/src/test/java/org/lucares/collections/Sparse2DLongArrayTest.java @@ -0,0 +1,109 @@ +package org.lucares.collections; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class Sparse2DLongArrayTest { + @Test + public void testPutValues() { + final Sparse2DLongArray matrix2d = new Sparse2DLongArray(); + + // put values + for (long i = 1; i < 5; i++) { + for (long j = 1; j < 5; j++) { + matrix2d.put(i, j, i * j); + } + } + Assertions.assertEquals(" 0 0 0 0 0\n" + // + " 0 1 2 3 4\n" + // + " 0 2 4 6 8\n" + // + " 0 3 6 9 12\n" + // + " 0 4 8 12 16\n", // + matrix2d.toString(120)); + + // update values + for (long i = 1; i < 5; i++) { + for (long j = 1; j < 5; j++) { + long value = matrix2d.get(i, j); + matrix2d.put(i, j, value * 2); + } + } + + // get values + for (long i = 1; i < 5; i++) { + for (long j = 1; j < 5; j++) { + final long expectedValue = i * j * 2; + final long actualValue = matrix2d.get(i, j); + Assertions.assertEquals(expectedValue, actualValue, "value at position " + i + "x" + j); + } + } + + Assertions.assertEquals(" 0 0 0 0 0\n" + // + " 0 2 4 6 8\n" + // + " 0 4 8 12 16\n" + // + " 0 6 12 18 24\n" + // + " 0 8 16 24 32\n", // + matrix2d.toString(120)); + } + + @Test + public void testPutGetWithLargeIndexes() { + final Sparse2DLongArray matrix2d = new Sparse2DLongArray(); + final long value = 1; + final long x = Long.MAX_VALUE; + final long y = Long.MAX_VALUE; + matrix2d.put(x, y, value); + + long actualValue = matrix2d.get(x, y); + Assertions.assertEquals(value, actualValue, "value at position " + x + "x" + y); + + Assertions.assertEquals("(9223372036854775807, 9223372036854775807) = 1\n", // + matrix2d.toString(120)); + } + + @Test + public void testWithLimitedSize() { + final Sparse2DLongArray matrix2d = new Sparse2DLongArray(10, 10); + final long value = 1; + matrix2d.put(9, 9, value); + + + Assertions.assertThrows(IndexOutOfBoundsException.class, ()-> {matrix2d.put(10, 9, value);}); + Assertions.assertThrows(IndexOutOfBoundsException.class, ()-> {matrix2d.put(9, 10, value);}); + + } + + @Test + public void testToStringWhenNotEnougthSpace() { + final Sparse2DLongArray matrix2d = new Sparse2DLongArray(); + + for (long i = 1; i < 5; i++) { + for (long j = 1; j < 5; j++) { + matrix2d.put(i, j, i * j); + } + } + + // NOTE: this expectation relies on a deterministic order of forEach + // (which is used in toString(), even though the order is not deterministic. + // We can to this here, because we have very small indices and their hash + // value is identical to the index. But that could change with a different + // hash algorithm. + Assertions.assertEquals("(1, 1) = 1\n" + // + "(1, 2) = 2\n" + // + "(1, 3) = 3\n" + // + "(1, 4) = 4\n" + // + "(2, 1) = 2\n" + // + "(2, 2) = 4\n" + // + "(2, 3) = 6\n" + // + "(2, 4) = 8\n" + // + "(3, 1) = 3\n" + // + "(3, 2) = 6\n" + // + "(3, 3) = 9\n" + // + "(3, 4) = 12\n" + // + "(4, 1) = 4\n" + // + "(4, 2) = 8\n" + // + "(4, 3) = 12\n" + // + "(4, 4) = 16\n", // + matrix2d.toString(12)); + } +}