From 32f1d18dac9d5c8d1508d7f2a10c08eb6cfcbbcc Mon Sep 17 00:00:00 2001 From: Taras Vashchenko Date: Mon, 10 Jul 2023 19:22:07 +0300 Subject: [PATCH 1/4] Added initial implementation of LongMap --- pom.xml | 6 + .../opensource/longmap/LongMapImpl.java | 123 ++++++++++++++++-- 2 files changed, 120 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 36c092b..8280a74 100644 --- a/pom.xml +++ b/pom.xml @@ -32,5 +32,11 @@ 4.12 test + + org.projectlombok + lombok + 1.18.16 + compile + diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java index 2f0b54b..92ffda1 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -1,43 +1,148 @@ package de.comparus.opensource.longmap; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + public class LongMapImpl implements LongMap { + + private static final int DEFAULT_CAPACITY = 16; + + private List>> buckets; + + private int capacity; + + private int size; + + public LongMapImpl() { + this(DEFAULT_CAPACITY); + } + + public LongMapImpl(int capacity) { + this.capacity = capacity; + this.buckets = new ArrayList<>(capacity); + IntStream.range(0, capacity).forEach(i -> buckets.add(null)); + } + public V put(long key, V value) { - return null; + 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 value; } 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); } 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; } public boolean isEmpty() { - return false; + return buckets.stream().allMatch(AbstractCollection::isEmpty); } public boolean containsKey(long key) { - return false; + return get(key) != null; } public boolean containsValue(V value) { - return false; + return buckets.stream() + .anyMatch(bucket -> bucket.stream() + .anyMatch(entry -> entry.value == value) + ); } public long[] keys() { - return null; + return buckets.stream() + .flatMapToLong(bucket -> bucket.stream().mapToLong(entry -> entry.key)) + .distinct() + .toArray(); } public V[] values() { - return null; + //todo: might rework + return (V[]) buckets.stream() + .flatMap(bucket -> bucket.stream().map(entry -> entry.value)) + .toArray(); } public long size() { - return 0; + return size; } public void clear() { + buckets.stream() + .flatMap(Collection::stream) + .collect(Collectors.toList()) + .clear(); + } + private int getIndex(long key) { + return Math.abs(Objects.hashCode(key)) % capacity; } + + // todo: add resizing + // todo: add tests + @Data + @AllArgsConstructor + private class Entry { + + private final long key; + + private T value; + + } + } From eb98acaeffa5a07804ccb7c88d48fae6a570b8d0 Mon Sep 17 00:00:00 2001 From: Taras Vashchenko Date: Wed, 12 Jul 2023 16:42:12 +0300 Subject: [PATCH 2/4] Added unit tests for initial LongMap implementation --- pom.xml | 12 +- .../opensource/longmap/LongMapImpl.java | 15 +- .../opensource/longmap/LongMapImplTest.java | 129 ++++++++++++++++++ 3 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java diff --git a/pom.xml b/pom.xml index 8280a74..df1c82c 100644 --- a/pom.xml +++ b/pom.xml @@ -26,17 +26,17 @@ - - junit - junit - 4.12 - test - 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 92ffda1..8b31f3c 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -3,14 +3,11 @@ import lombok.AllArgsConstructor; import lombok.Data; -import java.util.AbstractCollection; import java.util.ArrayList; -import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; import java.util.stream.IntStream; public class LongMapImpl implements LongMap { @@ -90,7 +87,7 @@ public V remove(long key) { } public boolean isEmpty() { - return buckets.stream().allMatch(AbstractCollection::isEmpty); + return size == 0; } public boolean containsKey(long key) { @@ -99,6 +96,7 @@ public boolean containsKey(long key) { public boolean containsValue(V value) { return buckets.stream() + .filter(Objects::nonNull) .anyMatch(bucket -> bucket.stream() .anyMatch(entry -> entry.value == value) ); @@ -106,6 +104,7 @@ public boolean containsValue(V value) { public long[] keys() { return buckets.stream() + .filter(Objects::nonNull) .flatMapToLong(bucket -> bucket.stream().mapToLong(entry -> entry.key)) .distinct() .toArray(); @@ -114,6 +113,7 @@ public long[] keys() { public V[] values() { //todo: might rework return (V[]) buckets.stream() + .filter(Objects::nonNull) .flatMap(bucket -> bucket.stream().map(entry -> entry.value)) .toArray(); } @@ -124,9 +124,9 @@ public long size() { public void clear() { buckets.stream() - .flatMap(Collection::stream) - .collect(Collectors.toList()) - .clear(); + .filter(Objects::nonNull) + .forEach(LinkedList::clear); + size = 0; } private int getIndex(long key) { @@ -134,7 +134,6 @@ private int getIndex(long key) { } // todo: add resizing - // todo: add tests @Data @AllArgsConstructor private class Entry { 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..5d25a00 --- /dev/null +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java @@ -0,0 +1,129 @@ +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.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 testGet() { + // given + long expectedKey = 1L; + String expectedValue = "one"; + + // when + testMap.put(expectedKey, expectedValue); + String actualValue = testMap.get(expectedKey); + + // then + assertEquals(expectedValue, 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 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); + } + +} \ No newline at end of file From 86dadaa25956dfa7e3cee19706f3245f1a6b2060 Mon Sep 17 00:00:00 2001 From: Taras Vashchenko Date: Wed, 12 Jul 2023 16:42:12 +0300 Subject: [PATCH 3/4] Added unit tests for initial LongMap implementation --- .../opensource/longmap/LongMapImpl.java | 6 ++- .../opensource/longmap/LongMapImplTest.java | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java index 8b31f3c..9a37349 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -48,7 +48,11 @@ public V put(long key, V value) { bucket.add(new Entry<>(key, value)); size++; - return value; + return bucket.stream() + .filter(entry -> entry.key == key) + .findFirst() + .map(entry -> entry.value) + .orElse(null); } public V get(long key) { diff --git a/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java index 5d25a00..7f0e03a 100644 --- a/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java @@ -8,6 +8,7 @@ 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; @@ -36,6 +37,24 @@ void testPut() { 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 @@ -50,6 +69,15 @@ void testGet() { assertEquals(expectedValue, actualValue); } + @Test + void testGet_WhenEntryIsNotFound() { + // when + String actualValue = testMap.get(1L); + + // then + assertNull(actualValue); + } + @Test void testRemove() { // given @@ -64,6 +92,15 @@ void testRemove() { assertEquals(expectedValue, deletedValue); } + @Test + void testRemove_WhenEntryIsNotFound() { + // when + String deletedValue = testMap.remove(1L); + + // then + assertNull(deletedValue); + } + @Test void testIsEmpty() { // given From 3bd2b7e149a819cf84a8d63f94f71b7fd1ceee56 Mon Sep 17 00:00:00 2001 From: Taras Vashchenko Date: Thu, 13 Jul 2023 15:55:54 +0300 Subject: [PATCH 4/4] Added resize implementation --- .../opensource/longmap/LongMapImpl.java | 45 +++++++++++++++---- .../opensource/longmap/LongMapImplTest.java | 11 +++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java index 9a37349..c51a018 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -14,23 +14,33 @@ public class LongMapImpl implements LongMap { private static final int DEFAULT_CAPACITY = 16; - private List>> buckets; + 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); + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); } - public LongMapImpl(int capacity) { + 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) { + if (size >= capacity * loadFactor) { + resize(); + } + int index = getIndex(key); LinkedList> bucket = buckets.get(index); @@ -55,6 +65,7 @@ public V put(long key, V value) { .orElse(null); } + @Override public V get(long key) { int index = getIndex(key); LinkedList> bucket = buckets.get(index); @@ -70,6 +81,7 @@ public V get(long key) { return valueEntry.map(Entry::getValue).orElse(null); } + @Override public V remove(long key) { int index = getIndex(key); LinkedList> bucket = buckets.get(index); @@ -90,14 +102,17 @@ public V remove(long key) { return removedValue; } + @Override public boolean isEmpty() { return size == 0; } + @Override public boolean containsKey(long key) { return get(key) != null; } + @Override public boolean containsValue(V value) { return buckets.stream() .filter(Objects::nonNull) @@ -106,6 +121,7 @@ public boolean containsValue(V value) { ); } + @Override public long[] keys() { return buckets.stream() .filter(Objects::nonNull) @@ -114,22 +130,26 @@ public long[] keys() { .toArray(); } + @Override public V[] values() { - //todo: might rework return (V[]) buckets.stream() .filter(Objects::nonNull) .flatMap(bucket -> bucket.stream().map(entry -> entry.value)) .toArray(); } + @Override public long size() { return size; } + public int getCapacity() { + return capacity; + } + + @Override public void clear() { - buckets.stream() - .filter(Objects::nonNull) - .forEach(LinkedList::clear); + IntStream.range(0, buckets.size()).forEach(i -> buckets.set(i, null)); size = 0; } @@ -137,7 +157,16 @@ private int getIndex(long key) { return Math.abs(Objects.hashCode(key)) % capacity; } - // todo: add resizing + 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 { diff --git a/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java index 7f0e03a..ead7a7b 100644 --- a/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java @@ -163,4 +163,15 @@ void testClear() { 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