add Sparse2DLongArray
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package org.lucares.collections;
|
||||
|
||||
public interface BiLongObjectFunction<O> {
|
||||
O apply(long key, O value);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.lucares.collections;
|
||||
|
||||
public interface LongObjConsumer<O> {
|
||||
void accept(long key, O value);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}×{@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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user