diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java index 2f0b54b..628e37c 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -1,43 +1,286 @@ package de.comparus.opensource.longmap; +import java.util.*; + public class LongMapImpl implements LongMap { + + static final int DEFAULT_INITIAL_CAPACITY = 16; + static final int MAXIMUM_CAPACITY = 67_108_864; + static final float DEFAULT_LOAD_FACTOR = 0.75f; + private Node[] table; + private int threshold; + private int size; + + public LongMapImpl() { + } + + public LongMapImpl(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + public LongMapImpl(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) { + throw new IllegalArgumentException("Bad initial capacity: " + initialCapacity); + } + if (initialCapacity > MAXIMUM_CAPACITY) { + initialCapacity = MAXIMUM_CAPACITY; + } + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Wrong load factor: " + loadFactor); + + this.threshold = (int)(initialCapacity * loadFactor); + } + + static class Node { + + final int hash; + + final long key; + + V value; + + Node next; + + Node(int hash, long key, V value, LongMapImpl.Node next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + public final long getKey() { + return key; + } + + public final V getValue() { + return value; + } + + public final V setValue(V value) { + V oldValue = this.value; + this.value = value; + return oldValue; + } + + public final String toString() { + return key + "=" + value; + } + + public int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Node) { + Node e = (Node) obj; + return key == e.getKey() && Objects.equals(value, e.getValue()); + } + return false; + } + } + + static int hash(long key) { + return Long.hashCode(key); + } + public V put(long key, V value) { + return putValue(hash(key), key, value); + } + + private V putValue(int hash, long key, V value) { + int index; + if ((table == null || table.length == 0) || size >= threshold) { + resize(); + } + + Node firstEl = table[index = hash % table.length]; + if (firstEl == null) { + // create new el in bucket + table[index] = new Node(hash, key, value, null); + size++; + } else { + Node element; + if (firstEl.hash == hash && firstEl.key == key) { + // update element if equals + element = firstEl; + } else { + while (true) { + // check next element + if (firstEl.next == null) { + firstEl.next = new Node(hash, key, value, null); + size++; + return null; + } + element = firstEl.next; + if (element.hash == hash && element.key == key) break;// exit the loop if the element matches + firstEl = element; + } + } + + return element.setValue(value); + } return null; } public V get(long key) { + LongMapImpl.Node el = getNode(key); + return el == null ? null : el.value; + } + + private Node getNode(long key) { + if (table == null) return null; + int hash = hash(key); + LongMapImpl.Node first = table[hash % table.length]; + if (first != null) { + // always check first node + if (first.hash == hash && first.key == key) return first; + Node element = first.next; + if (element != null) { + do { + if (element.hash == hash && element.key == key) return element; + } while ((element = element.next) != null); + } + } return null; } + private void resize() { + Node[] oldTab = table; + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap = oldCap * 2; + int newThr = oldThr * 2; + + if(newCap <= 0 || newThr <= 0) { + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + + @SuppressWarnings("unchecked") + Node[] newTable = (Node[]) new Node[newCap]; + threshold = newThr; + table = newTable; + + if (oldTab != null) { + + for (int i = 0; i < oldCap; ++i) { + Node element; + if ((element = oldTab[i]) != null) { + oldTab[i] = null; + if (element.next == null) { + newTable[element.hash % newCap] = element; + } else { + while (element != null) { + Node next = element.next; + int index = element.hash % newCap; + + element.next = newTable[index]; + newTable[index] = element; + element = next; + } + } + + } + } + } + } + + public V remove(long key) { + Node element; + if((element = removeVal(key, hash(key))) != null) { + return element.value; + } + return null; + } + + private Node removeVal(long key, int hash) { + Node firstElement; // first element of bucket + int index; + if (table != null && table.length > 0 && (firstElement = table[index = hash % table.length]) != null) { + Node node = null; + Node element; + + // check first element + if (firstElement.hash == hash && firstElement.key == key) { + node = firstElement; + } else if ((element = firstElement.next) != null) { + // looking for element + do { + if (element.hash == hash && element.key == key) { + node = element; + break; + } + } while ((element = element.next) != null); + } + + if (node != null) { + if (node == firstElement) { + table[index] = node.next; + } else { + firstElement.next = node.next; + } + --size; + return node; + } + } return null; } public boolean isEmpty() { - return false; + return size == 0; } public boolean containsKey(long key) { - return false; + return getNode(key) != null; } public boolean containsValue(V value) { + if (size > 0) { + for (Node node : table) { + for (; node != null; node = node.next) { + if (Objects.equals(value, node.value)) return true; + } + } + } return false; } public long[] keys() { - return null; + long[] keys = new long[(int) size()]; + int i = 0; + for (Node node : table) { + for (; node != null; node = node.next, i++) { + keys[i] = node.key; + } + } + return keys; } public V[] values() { - return null; + @SuppressWarnings("unchecked") + V[] values = (V[]) new Object[size]; + int i = 0; + for (Node node : table) { + for (; node != null; node = node.next, i++) { + values[i] = node.value; + } + } + return values; } public long size() { - return 0; + return size; } public void clear() { - + Node[] currentTable; + if ((currentTable = table) != null && size > 0) { + size = 0; + Arrays.fill(currentTable, null); + } } } diff --git a/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java new file mode 100644 index 0000000..e383830 --- /dev/null +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java @@ -0,0 +1,241 @@ +package de.comparus.opensource.longmap; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.IntStream; + +import static org.junit.Assert.*; + +public class LongMapImplTest { + + @Test + public void testDefConstructor() { + // setup + LongMap longMap = new LongMapImpl<>(); + // verify & execute + assertTrue(longMap.isEmpty()); + } + + @Test + public void testConstructorWithCapacity() { + // setup + LongMap longMap = new LongMapImpl<>(16); + // verify & execute + assertTrue(longMap.isEmpty()); + } + + @Test + public void testConstructorWithCapacityAndLoadFactory() { + // setup + LongMap longMap = new LongMapImpl<>(16, 0.75f); + // verify & execute + assertTrue(longMap.isEmpty()); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorWithWrongCapacityAndCorrectLoadFactory() { + new LongMapImpl<>(-16, 0.75f); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorWithCorrectCapacityAndWrongLoadFactory() { + new LongMapImpl<>(16, -0.75f); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorWithWrongCapacityAndWrongLoadFactory() { + new LongMapImpl<>(-16, -0.75f); + } + + @Test + public void testPutAndGet() { + // setup + LongMap longMap = new LongMapImpl<>(); + longMap.put(1, "First"); + longMap.put(2, "Second"); + + // verify & execute + assertEquals(2, longMap.size()); + assertEquals("First", longMap.get(1)); + assertEquals("Second", longMap.get(2)); + } + + @Test + public void testUpdateValue() { + // setup + LongMap longMap = new LongMapImpl<>(); + + // execute + longMap.put(1, "First"); + longMap.put(1, "Second"); + + // verify & execute + assertEquals(1, longMap.size()); + assertEquals("Second", longMap.get(1)); + } + + @Test + public void testPutAndGetWithCollision() { + // setup + LongMap longMap = new LongMapImpl<>(); + + // execute + longMap.put(17, "first"); + longMap.put(33, "second"); + + // execute & verify + assertEquals("first", longMap.get(17)); + assertEquals("second", longMap.get(33)); + } + + @Test + public void testGetWhenElementIsntExist() { + assertNull(new LongMapImpl<>().get(1)); + } + + @Test + public void testRemove() { + LongMap longMap = new LongMapImpl<>(); + longMap.put(1, "First"); + longMap.put(2, "Second"); + + longMap.remove(1); + + assertEquals(1, longMap.size()); + assertNull(longMap.get(1)); + assertEquals("Second", longMap.get(2)); + } + + @Test + public void testRemoveWithCollision1() { + LongMap longMap = new LongMapImpl<>(); + longMap.put(17, "first"); + longMap.put(33, "second"); + + longMap.remove(17); + + assertNull(longMap.get(17)); + assertEquals("second", longMap.get(33)); + } + + @Test + public void testRemoveWithCollision2() { + LongMap longMap = new LongMapImpl<>(); + longMap.put(17, "first"); + longMap.put(33, "second"); + + longMap.remove(33); + + assertNull(longMap.get(33)); + assertEquals("first", longMap.get(17)); + } + + @Test + public void testClear() { + LongMap longMap = new LongMapImpl<>(); + longMap.put(1, "First"); + longMap.put(2, "Second"); + + assertEquals(2, longMap.size()); + assertEquals("First", longMap.get(1)); + assertEquals("Second", longMap.get(2)); + + longMap.clear(); + assertTrue(longMap.isEmpty()); + assertEquals(0, longMap.size()); + assertNull("First", longMap.get(1)); + assertNull("Second", longMap.get(2)); + } + + @Test + public void testContainsKeys() { + // setup + LongMap longMap = new LongMapImpl<>(); + longMap.put(1, "First"); + longMap.put(2, "Second"); + + // verify & execute + assertTrue(longMap.containsKey(1)); + assertTrue(longMap.containsKey(2)); + assertFalse(longMap.containsKey(3)); + } + @Test + public void testContainsKeysWithCollision() { + // setup + LongMap longMap = new LongMapImpl<>(); + longMap.put(17, "first"); + longMap.put(33, "second"); + + assertTrue(longMap.containsKey(17)); + assertTrue(longMap.containsKey(33)); + } + + @Test + public void testContainsValues() { + // setup + LongMap longMap = new LongMapImpl<>(); + longMap.put(1, "First"); + longMap.put(2, "Second"); + + // verify & execute + assertTrue(longMap.containsValue("First")); + assertTrue(longMap.containsValue("Second")); + assertFalse(longMap.containsValue("Third")); + } + + @Test + public void testContainsValuesWithCollision() { + // setup + LongMap longMap = new LongMapImpl<>(); + longMap.put(17, "first"); + longMap.put(33, "second"); + + // verify & execute + assertTrue(longMap.containsValue("first")); + assertTrue(longMap.containsValue("second")); + assertFalse(longMap.containsValue("third")); + } + + @Test + public void testKeys() { + // setup + LongMap longMap = new LongMapImpl<>(); + int size = 10; + long[] expectedKeys = new long[size]; + IntStream.range(0, size).forEach(i -> { + longMap.put(i, "el-" + i); + expectedKeys[i] = i; + }); + + // execute + long[] keys = longMap.keys(); + + // verify + assertEquals(size, keys.length); + IntStream.range(0, size) + .forEach(i -> assertEquals(expectedKeys[i], keys[i])); + } + + @Test + public void testValues() { + // setup + LongMap longMap = new LongMapImpl(); + int size = 100; + String[] expectedValues = new String[size]; + IntStream.range(0, size).forEach(i -> { + longMap.put(i, "el-" + i); + expectedValues[i] = "el-" + i; + }); + + // execute + Object[] values = longMap.values(); + + // verify + assertEquals(size, values.length); + IntStream.range(0, size) + .forEach(i -> assertEquals(expectedValues[i], values[i])); + } +}