From 1ef036396fe00ec9cb48a35638817fbde53131bd Mon Sep 17 00:00:00 2001 From: Yuliia Dolnikova Date: Sat, 28 Oct 2023 13:44:15 +0300 Subject: [PATCH] Long map implementation --- .../comparus/opensource/longmap/LongMap.java | 73 ++++- .../opensource/longmap/LongMapImpl.java | 276 ++++++++++++++++- .../opensource/longmap/LongMapImplTest.java | 289 ++++++++++++++++++ 3 files changed, 631 insertions(+), 7 deletions(-) create mode 100644 src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java diff --git a/src/main/java/de/comparus/opensource/longmap/LongMap.java b/src/main/java/de/comparus/opensource/longmap/LongMap.java index adbf242..d333f00 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMap.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMap.java @@ -1,17 +1,88 @@ package de.comparus.opensource.longmap; +/** + * Collection of key-value pairs, where key - is a unique element by which value can be received. + * Key is represented by long type, and value can be any. + * + * @param - value type + */ public interface LongMap { + + /** + * Method inserts a pair of key-value into a collection. + * If some value by the passed key is already present in the collection, it's replaced by the {@param value}. + * If map is full for more than or equal to allowed loadFactor, then the rehashing occurs. + * + * @param key - unique key to be associated with the {@param value} + * @param value - value to be associated with the {@param key} + * @return a previous value if it was replaced by the new one, or {@code null}. + */ V put(long key, V value); + + /** + * Method searches for the value by the passed key. + * + * @param key - unique key to search its associated value + * @return key's value if it exists, otherwise - return {@code null} + */ V get(long key); + + /** + * Method removes a pair of key-value by its key. + * + * @param key - unique key by which a pair of key-value should be removed + * @return deleted value that was associated with the {@param key}, + * or {@code null} when such key wasn't exist in the collection + */ V remove(long key); + /** + * Method verifies whether a map contains any pairs or not. + * + * @return {@code true} when collection does not contain any elements, otherwise return {@code false} + */ boolean isEmpty(); + + /** + * Method verifies whether a map contains a passed key or not. + * + * @param key - unique key to verify its existence + * @return {@code true} when collection does not contain such key, otherwise return {@code false} + */ boolean containsKey(long key); + + /** + * Method verifies whether a map contains a passed value or not. + * + * @param value - value to verify its existence + * @return {@code true} when collection does not contain such value, otherwise return {@code false} + */ boolean containsValue(V value); - long[] keys(); + /** + * Method returns an array that contains all maps' keys. + * + * @return all maps' keys + */ + Long[] keys(); + + /** + * Method returns an array that contains all maps' values. + * + * @return all maps' values + */ V[] values(); + /** + * Method returns a number of all pairs in the map. + * + * @return map's size + */ long size(); + + /** + * Method removes all pairs from the map. + */ void clear(); + } diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java index 2f0b54b..f47a119 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -1,43 +1,307 @@ package de.comparus.opensource.longmap; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; + public class LongMapImpl implements LongMap { + + public static final int DEFAULT_CAPACITY = 16; + public static final int DEFAULT_LOAD_FACTOR = 75; + + private final Class valueClass; + private final int loadFactor; + private LongEntry[] table; + private int capacity; + + private int size = 0; + + + public LongMapImpl(Class valueClass) { + this.valueClass = valueClass; + this.capacity = DEFAULT_CAPACITY; + this.loadFactor = DEFAULT_LOAD_FACTOR; + this.table = (LongEntry[]) new LongEntry[this.capacity]; + } + + public LongMapImpl(Class valueClass, final int capacity) { + this.valueClass = valueClass; + this.capacity = capacity; + this.loadFactor = DEFAULT_LOAD_FACTOR; + this.table = (LongEntry[]) new LongEntry[this.capacity]; + } + + public LongMapImpl(Class valueClass, final int capacity, final int loadFactor) { + this.valueClass = valueClass; + this.capacity = capacity; + this.loadFactor = loadFactor; + this.table = (LongEntry[]) new LongEntry[this.capacity]; + } + + + @Override public V put(long key, V value) { + if (getTableLoadingPercentage() >= this.loadFactor) { + resize(); + } + return putEntry(key, value); + } + + /** + * Method calculates on which percentage the table is loaded. + * The calculation occurs by the next proportion: + * capacity - 100% (the maximum number of elements in the map) + * size - x% (the real number of elements in the map) + * + * @return percentage of map loading + */ + private int getTableLoadingPercentage() { + return this.size * 100 / this.capacity; + } + + /** + * Method rehashes maps' pairs by doubling the table's capacity, + * and coping all pairs from the previous table into a new increased table + * with recalculating their indexes. + */ + private void resize() { + LongEntry[] existentTable = Arrays.copyOf(this.table, this.capacity); + this.size = 0; + this.capacity = this.capacity * 2; + this.table = (LongEntry[]) new LongEntry[this.capacity]; + for (LongEntry longEntry : existentTable) { + LongEntry currentLongEntry = longEntry; + while (currentLongEntry != null) { + putEntry(currentLongEntry.getKey(), currentLongEntry.getValue()); + currentLongEntry = currentLongEntry.getNext(); + } + } + } + + /** + * Method inserts a pair of key-value into a collection. + * If some value by the passed key is already present in the collection, it's replaced by the {@param value}. + * + * @param key - unique key to be associated with the {@param value} + * @param value - value to be associated with the {@param key} + * @return a previous value if it was replaced by the new one, or {@code null}. + */ + private V putEntry(long key, V value) { + int index = getIndex(key); + LongEntry newLongEntry = new LongEntry<>(key, value, null); + + LongEntry currentLongEntry = this.table[index]; + if (currentLongEntry == null) { + this.table[index] = newLongEntry; + this.size++; + return null; + } + + LongEntry previousLongEntry = null; + while (currentLongEntry != null) { + if (currentLongEntry.getKey() == key) { + V previousValue = currentLongEntry.getValue(); + currentLongEntry.setValue(value); + return previousValue; + } + previousLongEntry = currentLongEntry; + currentLongEntry = currentLongEntry.getNext(); + } + previousLongEntry.setNext(newLongEntry); + this.size++; + return null; } + + @Override public V get(long key) { + int index = getIndex(key); + LongEntry currentLongEntry = this.table[index]; + if (currentLongEntry == null) { + return null; + } else if (currentLongEntry.getKey() == key) { + return currentLongEntry.getValue(); + } + + while (currentLongEntry != null) { + if (currentLongEntry.getKey() == key) { + return currentLongEntry.getValue(); + } + currentLongEntry = currentLongEntry.getNext(); + } return null; } + @Override public V remove(long key) { + int index = getIndex(key); + LongEntry previousLongEntry = null; + LongEntry currentLongEntry = this.table[index]; + + while (currentLongEntry != null) { + if (currentLongEntry.getKey() != key) { + previousLongEntry = currentLongEntry; + currentLongEntry = currentLongEntry.getNext(); + continue; + } + V removedValue = currentLongEntry.getValue(); + if (previousLongEntry != null) { + previousLongEntry.setNext(null); + } else { + this.table[index] = null; + } + this.size--; + return removedValue; + } + return null; } + @Override public boolean isEmpty() { - return false; + return this.size == 0; } + @Override public boolean containsKey(long key) { - return false; + Predicate> keyPredicate = currentLongEntry -> currentLongEntry.getKey() == key; + return containsByPredicate(keyPredicate); } + @Override public boolean containsValue(V value) { + Predicate> valuePredicate = currentLongEntry -> Objects.equals(currentLongEntry.getValue(), value); + return containsByPredicate(valuePredicate); + } + + /** + * Method verifies the existence of element by the passed {@param predicate}. + * + * @param predicate - predicate to check the existence + * @return {@code true} when some element by such {@param predicate} exists, otherwise - return {@code false}. + */ + private boolean containsByPredicate(Predicate> predicate) { + for (LongEntry longEntry : this.table) { + LongEntry currentLongEntry = longEntry; + while (currentLongEntry != null) { + if (predicate.test(currentLongEntry)) { + return true; + } + currentLongEntry = currentLongEntry.getNext(); + } + } return false; } - public long[] keys() { - return null; + + @Override + public Long[] keys() { + Long[] longKeys = new Long[this.size]; + Function, Long> getKey = LongEntry::getKey; + return getElements(longKeys, getKey); } + @Override public V[] values() { - return null; + V[] values = (V[]) Array.newInstance(this.valueClass, this.size); + Function, Object> getValue = LongEntry::getValue; + return (V[]) getElements(values, getValue); + } + + /** + * Method fills the {@param arrayToFill} by the elements retrieved with {@param entryFunction}. + * + * @param arrayToFill - array that should be filled + * @param entryFunction - function to receive an element that should be stored on the {@param arrayToFill} + * @param array type + * @return filled array + */ + private T[] getElements(T[] arrayToFill, Function, T> entryFunction) { + int currentIndex = 0; + for (LongEntry longEntry : this.table) { + LongEntry currentLongEntry = longEntry; + while (currentLongEntry != null) { + arrayToFill[currentIndex++] = entryFunction.apply(currentLongEntry); + currentLongEntry = currentLongEntry.getNext(); + } + } + return arrayToFill; } + + @Override public long size() { - return 0; + return this.size; } + @Override public void clear() { + this.size = 0; + this.table = (LongEntry[]) new LongEntry[DEFAULT_CAPACITY]; + } + + /** + * Method calculates a table index for the passed key. + * + * @param key - unique key to find its index + * @return a key's index + */ + private int getIndex(Long key) { + if (key == null) { + return 0; + } + return Math.abs(key.hashCode() % capacity); + } + + + /** + * Class represents key-value pairs, where key - is a unique element by which value can be received. + * Each entry store the unique key, its associated value and the link to the next element, which can be null. + * + * @param - value type + */ + public static class LongEntry { + private final long key; + private V value; + private LongEntry next; + + LongEntry(long key, V value, LongEntry next) { + this.key = key; + this.value = value; + this.next = next; + } + + public final long getKey() { + return key; + } + + public final V getValue() { + return value; + } + + public LongEntry getNext() { + return next; + } + + public void setValue(V value) { + this.value = value; + } + + public void setNext(LongEntry next) { + this.next = next; + } + + @Override + public String toString() { + return "LongEntry{" + + "key=" + key + + ", value=" + value + + '}'; + } } + } 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..0d78fa8 --- /dev/null +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java @@ -0,0 +1,289 @@ +package de.comparus.opensource.longmap; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class LongMapImplTest { + + @Test + public void putShouldInsertNewValueWhenMapIsEmpty() { + // GIVEN + int newKey = 1; + String newValue = "firstValue"; + LongMap longMap = new LongMapImpl<>(String.class); + + // WHEN + String previousValue = longMap.put(newKey, newValue); + + // THEN + assertNull(previousValue); + assertFalse(longMap.isEmpty()); + assertSame(1L, longMap.size()); + assertEquals(newValue, longMap.get(newKey)); + } + + @Test + public void putShouldReplaceValueByKeyWhenMapContainsSuchKey() { + // GIVEN + int existentKey = 1; + LongMap longMap = new LongMapImpl<>(String.class); + String expectedPreviousValue = "firstValue"; + longMap.put(existentKey, expectedPreviousValue); + + // WHEN + String newValue = "secondValue"; + String actualPreviousValue = longMap.put(existentKey, newValue); + + // THEN + assertNotNull(actualPreviousValue); + assertEquals(expectedPreviousValue, actualPreviousValue); + + assertFalse(longMap.isEmpty()); + assertSame(1L, longMap.size()); + assertEquals(newValue, longMap.get(existentKey)); + } + + @Test + public void putShouldInsertMultipleValuesByUniqueKeys() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + + // WHEN + List expectedKeys = Arrays.asList(1L, 2L, 3L, 4L, 11L); + expectedKeys.forEach(key -> longMap.put(key, "val" + key)); + + // THEN + assertFalse(longMap.isEmpty()); + assertSame(5L, longMap.size()); + } + + @Test + public void putShouldInsertMultipleValuesByUniqueKeysAndRehashElementsWhenMapIsAlmostFull() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class, 3, 25); + + // WHEN + List expectedKeys = Arrays.asList(1L, 2L, 3L, 4L, 11L); + expectedKeys.forEach(key -> longMap.put(key, "val" + key)); + + // THEN + assertFalse(longMap.isEmpty()); + assertSame(5L, longMap.size()); + } + + + @Test + public void getShouldReturnValueByKeyWhenSuchKeyExists() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + longMap.put(1, "val1"); + String expectedValue = "val2"; + longMap.put(2, expectedValue); + + // WHEN + String actualValue = longMap.get(2); + + // THEN + assertNotNull(actualValue); + assertEquals(expectedValue, actualValue); + } + + @Test + public void getShouldReturnNullWhenSuchKeyDoesNotExist() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + longMap.put(1, "val1"); + longMap.put(2, "val2"); + + // WHEN + String actualValue = longMap.get(333); + + // THEN + assertNull(actualValue); + } + + + @Test + public void removeShouldDeleteEntryByKeyAndReturnItsValueWhenSuchKeyExists() { + // GIVEN + long keyToBeRemoved = 2L; + String expectedValue = "val2"; + List expectedKeys = Arrays.asList(1L, keyToBeRemoved, 3L, 4L, 11L); + LongMap longMap = new LongMapImpl<>(String.class); + expectedKeys.forEach(key -> longMap.put(key, "val" + key)); + + // WHEN + String actualValue = longMap.remove(keyToBeRemoved); + + // THEN + assertNotNull(actualValue); + assertEquals(expectedValue, actualValue); + assertSame(4L, longMap.size()); + assertFalse(longMap.containsKey(keyToBeRemoved)); + } + + @Test + public void removeShouldNotDeleteEntryByKeyAndShouldReturnNullWhenSuchKeyDoesNotExist() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + longMap.put(1, "val1"); + longMap.put(2, "val2"); + + // WHEN + String actualValue = longMap.remove(333); + + // THEN + assertNull(actualValue); + assertSame(2L, longMap.size()); + } + + + @Test + public void isEmptyShouldReturnTrueWhenMapIsEmpty() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + + // WHEN // THEN + assertTrue(longMap.isEmpty()); + } + + @Test + public void isEmptyShouldReturnFalseWhenMapIsNotEmpty() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + longMap.put(1, "val1"); + + // WHEN // THEN + assertFalse(longMap.isEmpty()); + } + + + @Test + public void containsKeyShouldReturnTrueWhenKeyExists() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + longMap.put(1, "val1"); + longMap.put(2, null); + longMap.put(3, "val3"); + + // WHEN // THEN + assertTrue(longMap.containsKey(1)); + assertTrue(longMap.containsKey(2)); + assertTrue(longMap.containsKey(3)); + } + + @Test + public void containsKeyShouldReturnFalseWhenKeyDoesNotExist() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + longMap.put(1, "val1"); + + // WHEN // THEN + assertFalse(longMap.containsKey(333)); + } + + + @Test + public void containsValueShouldReturnTrueWhenValueExists() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + longMap.put(1, "val1"); + longMap.put(3, "val3"); + longMap.put(4, "val4"); + longMap.put(11, null); + + // WHEN // THEN + assertTrue(longMap.containsValue("val4")); + assertTrue(longMap.containsValue(null)); + } + + @Test + public void containsValueShouldReturnFalseWhenValueDoesNotExist() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + longMap.put(1, "val1"); + longMap.put(3, "val3"); + longMap.put(4, "val4"); + longMap.put(11, "val11"); + + // WHEN // THEN + assertFalse(longMap.containsValue("333")); + } + + + @Test + public void keysShouldReturnAllExistentKeys() { + // GIVEN + List expectedKeys = Arrays.asList(1L, 2L, 3L, 4L, 11L); + LongMap longMap = new LongMapImpl<>(String.class); + expectedKeys.forEach(key -> longMap.put(key, "val" + key)); + + // WHEN + Long[] actualKeys = longMap.keys(); + + // THEN + assertNotNull(actualKeys); + assertTrue(expectedKeys.containsAll(Arrays.asList(actualKeys))); + } + + @Test + public void valuesShouldReturnAllExistentValues() { + // GIVEN + List expectedValues = Stream.of("1", "2", "3", "4", "11").collect(Collectors.toList()); + LongMap longMap = new LongMapImpl<>(String.class); + expectedValues.forEach(value -> longMap.put(Integer.parseInt(value), value)); + longMap.put(7, null); + expectedValues.add(null); + + // WHEN + String[] actualValues = longMap.values(); + + // THEN + assertNotNull(actualValues); + assertTrue(expectedValues.containsAll(Arrays.asList(actualValues))); + } + + + @Test + public void sizeShouldReturnAnActualMapSize() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + long expectedSize = 2; + + longMap.put(1, "val1"); + longMap.put(2, "val2"); + + // WHEN + long actualSize = longMap.size(); + + // THEN + assertSame(expectedSize, actualSize); + } + + + @Test + public void clearShouldDeleteAllEntries() { + // GIVEN + LongMap longMap = new LongMapImpl<>(String.class); + longMap.put(1, "val1"); + longMap.put(2, "val2"); + + // WHEN + longMap.clear(); + + // THEN + assertTrue(longMap.isEmpty()); + } + +}