add Sparse2DLongArray

This commit is contained in:
2019-12-26 15:44:58 +01:00
parent ec7a03f068
commit c6f5a3d2fe
7 changed files with 940 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
package org.lucares.collections;
public interface BiLongObjectFunction<O> {
O apply(long key, O value);
}

View File

@@ -0,0 +1,5 @@
package org.lucares.collections;
public interface LongObjConsumer<O> {
void accept(long key, O value);
}

View File

@@ -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<V> {
// 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.
* <p>
* 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<V> initialValueIfAbsent, final BiLongObjectFunction<V> 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<V> 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.
* <p>
* Note: this method is slower than {@link #forEach(LongLongConsumer)}.
*
* @param consumer the consumer
*/
public void forEachOrdered(final LongObjConsumer<V> 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));
}
}

View File

@@ -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<LongLongHashMap> matrix;
private final Supplier<LongLongHashMap> 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<LongLongHashMap>(initialCapacity, loadFactor);
initialValueSupplier = () -> new LongLongHashMap(initialCapacity, loadFactor);
}
/**
* Add or overwrite a value at position {@code x}&times;{@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}&times;{@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();
}
}

View File

@@ -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}&times;{@code y}
*/
public void apply(long x, long y, long value);
}

View File

@@ -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<Long>{
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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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;
}
}

View File

@@ -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));
}
}