diff --git a/pom.xml b/pom.xml index 36c092b..df1c82c 100644 --- a/pom.xml +++ b/pom.xml @@ -27,9 +27,15 @@ - junit - junit - 4.12 + org.projectlombok + lombok + 1.18.16 + compile + + + org.junit.jupiter + junit-jupiter + RELEASE test diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java index 2f0b54b..c51a018 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -1,43 +1,180 @@ package de.comparus.opensource.longmap; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.IntStream; + public class LongMapImpl implements LongMap { + + private static final int DEFAULT_CAPACITY = 16; + + private static final double DEFAULT_LOAD_FACTOR = 0.75; + + private final List>> buckets; + + private int capacity; + + private int size; + + private final double loadFactor; + + public LongMapImpl() { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + public LongMapImpl(int capacity, double loadFactor) { + this.capacity = capacity; + this.buckets = new ArrayList<>(capacity); + this.loadFactor = loadFactor; + IntStream.range(0, capacity).forEach(i -> buckets.add(null)); + } + + @Override public V put(long key, V value) { - return null; + if (size >= capacity * loadFactor) { + resize(); + } + + int index = getIndex(key); + LinkedList> bucket = buckets.get(index); + + if (bucket == null) { + bucket = new LinkedList<>(); + buckets.set(index, bucket); + } + + for (Entry entry : bucket) { + if (entry.getKey() == key) { + entry.setValue(value); + return entry.value; + } + } + + bucket.add(new Entry<>(key, value)); + size++; + return bucket.stream() + .filter(entry -> entry.key == key) + .findFirst() + .map(entry -> entry.value) + .orElse(null); } + @Override public V get(long key) { - return null; + int index = getIndex(key); + LinkedList> bucket = buckets.get(index); + + if (bucket == null) { + return null; + } + + Optional> valueEntry = bucket.stream() + .filter(entry -> entry.getKey() == key) + .findFirst(); + + return valueEntry.map(Entry::getValue).orElse(null); } + @Override public V remove(long key) { - return null; + int index = getIndex(key); + LinkedList> bucket = buckets.get(index); + + if (bucket == null) { + return null; + } + + V removedValue = null; + for (Entry entry : bucket) { + if (entry.getKey() == key) { + bucket.remove(entry); + size--; + removedValue = entry.value; + } + } + + return removedValue; } + @Override public boolean isEmpty() { - return false; + return size == 0; } + @Override public boolean containsKey(long key) { - return false; + return get(key) != null; } + @Override public boolean containsValue(V value) { - return false; + return buckets.stream() + .filter(Objects::nonNull) + .anyMatch(bucket -> bucket.stream() + .anyMatch(entry -> entry.value == value) + ); } + @Override public long[] keys() { - return null; + return buckets.stream() + .filter(Objects::nonNull) + .flatMapToLong(bucket -> bucket.stream().mapToLong(entry -> entry.key)) + .distinct() + .toArray(); } + @Override public V[] values() { - return null; + return (V[]) buckets.stream() + .filter(Objects::nonNull) + .flatMap(bucket -> bucket.stream().map(entry -> entry.value)) + .toArray(); } + @Override public long size() { - return 0; + return size; } + public int getCapacity() { + return capacity; + } + + @Override public void clear() { + IntStream.range(0, buckets.size()).forEach(i -> buckets.set(i, null)); + size = 0; + } + private int getIndex(long key) { + return Math.abs(Objects.hashCode(key)) % capacity; } + + private void resize() { + int newCapacity = capacity * 2; + int capacityDelta = newCapacity - capacity; + List>> newReservedBuckets = new ArrayList<>(capacityDelta); + + IntStream.range(0, capacityDelta).forEach(bucket -> newReservedBuckets.add(null)); + buckets.addAll(newReservedBuckets); + capacity = newCapacity; + } + + @Data + @AllArgsConstructor + private class Entry { + + private final long key; + + private T 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..ead7a7b --- /dev/null +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java @@ -0,0 +1,177 @@ +package de.comparus.opensource.longmap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +class LongMapImplTest { + + private LongMap testMap; + + @BeforeEach + void setUp() { + testMap = new LongMapImpl<>(); + } + + @Test + void testPut() { + // given + long[] expectedKeys = {1L, 2L, 3L}; + String[] expectedValues = {"one", "two", "three"}; + + // when + IntStream.range(0, expectedKeys.length) + .forEach(i -> testMap.put(expectedKeys[i], expectedValues[i])); + + // then + assertEquals(3, testMap.size()); + assertArrayEquals(expectedKeys, testMap.keys()); + assertArrayEquals(expectedValues, testMap.values()); + } + + @Test + void testPut_WhenEntryIsPresent() { + // given + long[] expectedKeys = {1L, 2L, 3L}; + String[] expectedValues = {"one", "two", "three"}; + + // when + IntStream.range(0, expectedKeys.length) + .forEach(i -> testMap.put(expectedKeys[i], expectedValues[i])); + testMap.put(expectedKeys[1], "twotwo"); + expectedValues[1] = "twotwo"; + + // then + assertEquals(3, testMap.size()); + assertArrayEquals(expectedKeys, testMap.keys()); + assertArrayEquals(expectedValues, testMap.values()); + } + + @Test + void testGet() { + // given + long expectedKey = 1L; + String expectedValue = "one"; + + // when + testMap.put(expectedKey, expectedValue); + String actualValue = testMap.get(expectedKey); + + // then + assertEquals(expectedValue, actualValue); + } + + @Test + void testGet_WhenEntryIsNotFound() { + // when + String actualValue = testMap.get(1L); + + // then + assertNull(actualValue); + } + + @Test + void testRemove() { + // given + long expectedKey = 1L; + String expectedValue = "one"; + + // when + testMap.put(expectedKey, expectedValue); + String deletedValue = testMap.remove(expectedKey); + + // then + assertEquals(expectedValue, deletedValue); + } + + @Test + void testRemove_WhenEntryIsNotFound() { + // when + String deletedValue = testMap.remove(1L); + + // then + assertNull(deletedValue); + } + + @Test + void testIsEmpty() { + // given + long expectedKey = 1L; + String expectedValue = "one"; + + // when + testMap.put(expectedKey, expectedValue); + boolean isEmpty = testMap.isEmpty(); + + // then + assertFalse(isEmpty); + } + + @Test + void testContainsKey() { + // given + long[] expectedKeys = {1L, 2L, 3L}; + String[] expectedValues = {"one", "two", "three"}; + + // when + IntStream.range(0, expectedKeys.length) + .forEach(i -> testMap.put(expectedKeys[i], expectedValues[i])); + boolean containsKey = testMap.containsKey(expectedKeys[1]); + + // then + assertTrue(containsKey); + } + + @Test + void testContainsValue() { + // given + long[] expectedKeys = {1L, 2L, 3L}; + String[] expectedValues = {"one", "two", "three"}; + + // when + IntStream.range(0, expectedKeys.length) + .forEach(i -> testMap.put(expectedKeys[i], expectedValues[i])); + boolean containsValue = testMap.containsValue(expectedValues[1]); + + // then + assertTrue(containsValue); + } + + @Test + void testClear() { + // given + long[] expectedKeys = {1L, 2L, 3L}; + String[] expectedValues = {"one", "two", "three"}; + + // when + IntStream.range(0, expectedKeys.length) + .forEach(i -> testMap.put(expectedKeys[i], expectedValues[i])); + boolean nonEmptyBeforeClear = !testMap.isEmpty(); + testMap.clear(); + boolean emptyAfterClear = testMap.isEmpty(); + + // then + assertTrue(nonEmptyBeforeClear); + assertTrue(emptyAfterClear); + } + + @Test + void testResize() { + LongMapImpl testResizeMap = new LongMapImpl<>(); + final int TEST_SIZE = 17; + final int EXPECTED_CAPACITY = 32; + IntStream.range(0, TEST_SIZE).forEach(i -> testResizeMap.put(++i, "test")); + + assertEquals(TEST_SIZE, testResizeMap.size()); + assertEquals(EXPECTED_CAPACITY, testResizeMap.getCapacity()); + } + +} \ No newline at end of file