diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..8ea8ffe --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,36 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/2.0/configuration-reference +version: 2.1 + +# Define a job to be invoked later in a workflow. +# See: https://circleci.com/docs/2.0/configuration-reference/#jobs +jobs: + # Below is the definition of your job to build and test your app, you can rename and customize it as you want. + build-and-test: + # These next lines define a Docker executor: https://circleci.com/docs/2.0/executor-types/ + # You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. + # Be sure to update the Docker image tag below to openjdk version of your application. + # A list of available CircleCI Docker Convenience Images are available here: https://circleci.com/developer/images/image/cimg/openjdk + docker: + - image: cimg/openjdk:11.0 + # Add steps to the job + # See: https://circleci.com/docs/2.0/configuration-reference/#steps + steps: + # Checkout the code as the first step. + - checkout + # Use mvn clean and package as the standard maven build phase + - run: + name: Build + command: mvn -B -DskipTests clean package + # Then run your tests! + - run: + name: Test + command: mvn test + +# Invoke jobs via workflows +# See: https://circleci.com/docs/2.0/configuration-reference/#workflows +workflows: + sample: # This is the name of the workflow, feel free to change it to better match your workflow. + # Inside the workflow, you define the jobs you want to run. + jobs: + - build-and-test diff --git a/.gitignore b/.gitignore index a870eaf..3a0f4e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea/ +.target/ long-map.iml \ No newline at end of file diff --git a/README.md b/README.md index 7bb1716..926ad3f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # long-map -Finish development of class LongMapImpl, which implements a map with keys of type long. It has to be a hash table (like HashMap). Requirements: -* it should not use any known Map implementations; -* it should use as less memory as possible and have adequate performance; -* the main aim is to see your codestyle and teststyle +## unit tests: [![CircleCI](https://dl.circleci.com/status-badge/img/gh/Artemiy7/long-map/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/Artemiy7/long-map/tree/master) + +## time complexity text: +I added 201 entries to the LongMap using different numbers and displayed the distribution of entries in buckets on the graph. +- ![#c5f015](https://placehold.co/15x15/c5f015/c5f015.png) Average case using new Random().nextlong() +- ![#f03c15](https://placehold.co/15x15/f03c15/f03c15.png) Worst case using numbers with same ending number e.g.: 7777, 7777777, 1113337 +- ![#1589F0](https://placehold.co/15x15/1589F0/1589F0.png) Log(201) for comparison + +![map](https://user-images.githubusercontent.com/83453822/235929762-c5aa80d2-5caf-4e80-a91c-43938790efdf.png) diff --git a/pom.xml b/pom.xml index 36c092b..ac33188 100644 --- a/pom.xml +++ b/pom.xml @@ -32,5 +32,10 @@ 4.12 test + + junit + junit + 4.13.2 + diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java index 2f0b54b..4a1b3d4 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -1,43 +1,324 @@ package de.comparus.opensource.longmap; +import java.lang.reflect.Array; +import java.util.*; + public class LongMapImpl implements LongMap { + + private final int DEFAULT_INITIAL_CAPACITY; + private final float LOAD_FACTOR; + + private transient LongEntry[] table; + /** The total number of entries in the map.*/ + + private int count; + /** The table is resized when its size exceeds this threshold. The value of this field is (int)(capacity * loadFactor)*/ + private int threshold; + + public LongMapImpl() { + this(16, 0.75f); + } + + /** + * Constructs a new map with the given LongMap. + */ + public LongMapImpl(LongMap longMap) { + this((int) (2*longMap.size()), 0.75f); + putAll(longMap); + } + + public LongMapImpl(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal argument capacity: " + initialCapacity); + if (loadFactor < 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load capacity: " + loadFactor); + + if (initialCapacity==0) + initialCapacity = 1; + + DEFAULT_INITIAL_CAPACITY = initialCapacity; + LOAD_FACTOR = loadFactor; + threshold = (int) (DEFAULT_INITIAL_CAPACITY * LOAD_FACTOR); + table = new LongEntry[DEFAULT_INITIAL_CAPACITY]; + count = 0; + } + + /** + * If number of keys in the table exceeds threshold + * method increases the table size to (oldSize * 2) + * and recalculates the index of every key in the table. + */ + @Override public V put(long key, V value) { - return null; + if (count >= threshold) { + resize(); + } + LongEntry newEntry = new LongEntry<>(key, value); + int index = calculateIndex(key); + return putEntry(newEntry, index); + } + + private V putEntry(LongEntry newEntry, int index) { + if (table[index] == null) { + table[index] = newEntry; + count++; + } else { + for (LongEntry entry = table[index]; entry != null;) { + if (entry.getKey() == newEntry.getKey()) { + entry.value = newEntry.value; + return newEntry.value; + } else if (entry.next == null) { + entry.next = newEntry; + count++; + break; + } + entry = entry.next; + } + } + return newEntry.value; + } + + /** + * Copies all the entries from the longMap to this table. + */ + public void putAll(LongMap longMap) { + if (longMap != null) { + long[] keys = longMap.keys(); + for (int i = keys.length; i-- > 0; ) { + put(keys[i], longMap.get(keys[i])); + } + } } + + private int calculateIndex(long key) { + if (key == 0) + return 0; + else + return (Long.hashCode(key) & 0x7FFFFFFF) % table.length; + } + + /** + * Resizes table and increases the capacity of table to (oldSize * 2). + * Recalculates the index of every key in order to make map operations more efficient. + * This method is called automatically when the + * number of keys in the table exceeds threshold. + */ + private void resize() { + int oldSize = this.table.length; + int newSize = oldSize * 2; + LongEntry[] newTable = new LongEntry[newSize]; + LongEntry[] oldTable = this.table; + + this.threshold = (int)(newSize * LOAD_FACTOR); + this.table = newTable; + + for (int i = oldSize; i-- > 0;) { + for (LongEntry entry = oldTable[i]; entry != null;) { + int index = calculateIndex(entry.getKey()); + LongEntry next = entry.next; + if (table[index] == null) { + table[index] = entry; + table[index].next = null; + } else { + for (LongEntry newTableEntry = table[index]; newTableEntry != null;) { + if (newTableEntry.next == null) { + newTableEntry.next = entry; + newTableEntry.next.next = null; + } + newTableEntry = newTableEntry.next; + } + } + entry = next; + } + } + } + + /** + * Searches for value by key in a table. + * @return a value if the key is present in the table + * @return null if the key is not found in the table + */ + @Override public V get(long key) { + int index = calculateIndex(key); + for (LongEntry entry = table[index]; entry != null;) { + if (key == entry.key) { + return entry.value; + } + entry = entry.next; + } return null; } + @Override public V remove(long key) { + int index = calculateIndex(key); + if (table[index] == null) + return null; + + LongEntry previous = null; + for (LongEntry entry = table[index]; entry != null;) { + if (key == entry.key) { + if (previous == null) + table[index] = table[index].next; + else + previous.next = entry.next; + count--; + return entry.value; + } + previous = entry; + entry = entry.next; + } return null; } + @Override public boolean isEmpty() { - return false; + return count == 0; } + @Override public boolean containsKey(long key) { - return false; + return get(key) != null; } + @Override public boolean containsValue(V value) { + for (int i = count; i-- > 0 ;) { + for (LongEntry entry = table[i]; entry != null ; entry = entry.next) { + if (entry.value.equals(value)) { + return true; + } + } + } return false; } + @Override public long[] keys() { - return null; + long[] keys = new long[count]; + int keysIndex = 0; + + for (int i = table.length; i-- > 0;) { + for (LongEntry entry = table[i]; entry != null;) { + keys[keysIndex] = entry.key; + keysIndex++; + entry = entry.next; + } + } + return keys; } - public V[] values() { - return null; + + /** + * @throws ArrayIndexOutOfBoundsException if map is empty + */ + @Override + public V[] values() throws ArrayIndexOutOfBoundsException { + V[] values = (V[]) new Object[count]; + int valueIndex = 0; + for (int i = table.length; i-- > 0;) { + for (LongEntry entry = table[i]; entry != null;) { + values[valueIndex] = entry.value; + valueIndex++; + entry = entry.next; + } + } + return (V[]) Array.newInstance(values[0].getClass(), values.length); } + @Override public long size() { - return 0; + return count; } + @Override public void clear() { + for (int i = table.length; i-- > 0;) { + table[i] = null; + } + count = 0; + System.gc(); + } + + public Set> entrySet() { + Set> entrySet = new HashSet<>(); + for (int i = table.length; i-- > 0;) { + for (LongEntry entry = table[i]; entry != null;) { + entrySet.add(entry); + entry = entry.next; + } + } + return entrySet; + } + + @Override + public int hashCode() { + return Arrays.hashCode(keys()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LongMapImpl longMap = (LongMapImpl) o; + return Arrays.equals(keys(), longMap.keys()); + } + + @Override + public String toString() { + int capacity = count; + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("{"); + for (int i = table.length; i-- > 0;) { + for (LongEntry entry = table[i]; entry != null;) { + stringBuilder.append(entry.toString()); + stringBuilder.append(", "); + entry = entry.next; + capacity--; + } + if (capacity == 0 && stringBuilder.length() > 1) + return stringBuilder.replace(stringBuilder.length()-2, stringBuilder.length(), "") + .append("}").toString(); + } + return "{}"; + } + + private static class LongEntry { + final long key; + V value; + LongEntry next; + + LongEntry(long key, V value) { + this.key = key; + this.value = value; + } + + public long getKey() { + return key; + } + + public V getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LongEntry longEntry = (LongEntry) o; + return key == longEntry.key; + } + + @Override + public int hashCode() { + return Objects.hash(key); + } + @Override + public String toString() { + return this.key + "=" + this.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..ef9ed3a --- /dev/null +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java @@ -0,0 +1,283 @@ +package de.comparus.opensource.longmap; + +import org.junit.Before; +import org.junit.Test; + +public class LongMapImplTest { + LongMapImpl longMap; + @Before + public void before() { + longMap = new LongMapImpl<>(); + } + + @Test + public void putEntry_Test() { + longMap.put(1000000000l, "Str"); + longMap.put(1000000001l, "Str1"); + longMap.put(1000000002l, "Str2"); + + assert longMap.get(1000000000l).equals("Str"); + assert longMap.get(1000000001l).equals("Str1"); + assert longMap.get(1000000002l).equals("Str2"); + assert longMap.size() == 3; + } + + @Test + public void putDuplicateValue_Test() { + longMap.put(1000000000l, "Str"); + longMap.put(1000000000l, "Str1"); + longMap.put(1000000000l, "Str2"); + longMap.put(1000000000l, "Str3"); + + assert longMap.get(1000000000l).equals("Str3"); + assert longMap.size() == 1; + } + + @Test + public void putDuplicateNullValue_Test() { + longMap.put(0, "Str"); + longMap.put(0, "Str1"); + longMap.put(0, "Str2"); + + assert longMap.get(0).equals("Str2"); + assert longMap.size() == 1; + } + + + @Test + public void put101Entries_Test() { + for (int i = 0; i < 101; i++) { + longMap.put((i * 3331111), "i = " + i); + } + assert longMap.size() == 101; + for (int i = 0; i < 101; i++) { + assert longMap.get(i * 3331111).equals("i = " + i); + } + } + + @Test + public void removeEntry_Test() { + longMap.put(1000000000l, "Str"); + longMap.put(1000000001l, "Str1"); + longMap.put(1000000002l, "Str2"); + + assert longMap.remove(1000000000l).equals("Str"); + assert longMap.remove(1000000001l).equals("Str1"); + assert longMap.remove(1000000002l).equals("Str2"); + assert longMap.size() == 0; + } + + @Test + public void removeEntry_NotOkTest() { + longMap.put(1000000000l, "Str"); + + assert longMap.remove(1000000001l) == null; + assert longMap.size() == 1; + } + + @Test + public void removeEntryFromEmptyMap_Test() { + assert longMap.remove(1000000001l) == null; + assert longMap.size() == 0; + } + + @Test + public void remove101Entries_Test() { + for (int i = 0; i < 101; i++) { + longMap.put((i * 3331111), "i = " + i); + } + assert longMap.size() == 101; + for (int i = 0; i < 101; i++) { + longMap.remove(i * 3331111).equals("i = " + i); + } + assert longMap.size() == 0; + } + + @Test + public void keys_Test() { + longMap = new LongMapImpl<>(); + longMap.put(1000000000l, "Str"); + + assert longMap.keys()[0] == 1000000000l; + assert longMap.size() == 1; + } + + @Test + public void keys101_Test() { + for (int i = 0; i < 101; i++) { + longMap.put((i * 3331111), "i = " + i); + } + long[] keys = longMap.keys(); + assert keys.length == 101; + for (int i = 0; i < keys.length; i++) { + longMap.remove(keys[i]); + } + assert longMap.size() == 0; + } + + @Test + public void values_Test() { + longMap.put(1000000000l, "Str"); + + assert longMap.keys()[0] == 1000000000l; + assert longMap.size() == 1; + } + + @Test + public void values101_Test() { + for (int i = 0; i < 101; i++) { + longMap.put((i * 3331111), "i = " + i); + } + String[] values = longMap.values(); + assert values.length == 101; + } + + @Test + public void containsKeyTest() { + longMap.put(1000000000l, "Str"); + + assert longMap.containsKey(1000000000l); + assert !longMap.containsKey(1000000001l); + } + + @Test + public void containsKey_NotOkTest() { + longMap.put(1000000000l, "Str"); + + assert !longMap.containsKey(1000000001l); + assert longMap.size() == 1; + } + + @Test + public void containsKey101_Test() { + for (int i = 0; i < 101; i++) { + longMap.put((i * 3331111), "i = " + i); + } + for (int i = 0; i < 101; i++) { + assert longMap.containsKey(i * 3331111); + } + } + + @Test + public void containsValue_Test() { + longMap.put(1000000000l, "Str"); + + assert longMap.containsValue("Str"); + } + + @Test + public void containsValue_NotOkTest() { + longMap.put(1000000000l, "Str"); + + assert !longMap.containsValue("Str1"); + assert longMap.size() == 1; + } + + @Test + public void containsValue101_Test() { + for (int i = 0; i < 101; i++) { + longMap.put((i * 3331111), "i = " + i); + } + for (int i = 0; i < 101; i++) { + assert longMap.containsKey(i * 3331111); + } + } + + @Test + public void putAll_Test() { + longMap = new LongMapImpl<>(); + longMap.put(10000000007l, "Str7"); + LongMapImpl insertLongMap = new LongMapImpl<>(); + insertLongMap.put(10000000008l, "Str8"); + insertLongMap.put(10000000009l, "Str9"); + insertLongMap.put(100000000010l, "Str10"); + + assert longMap.size() == 1; + longMap.putAll(insertLongMap); + assert longMap.size() == 4; + assert longMap.get(10000000007l).equals("Str7"); + assert longMap.get(10000000008l).equals("Str8"); + assert longMap.get(10000000009l).equals("Str9"); + assert longMap.get(100000000010l).equals("Str10"); + } + + @Test + public void putAllDuplicate_Test() { + longMap = new LongMapImpl<>(); + longMap.put(10000000007l, "Str7"); + LongMapImpl insertLongMap = new LongMapImpl<>(); + insertLongMap.put(10000000007l, "Str77"); + insertLongMap.put(10000000008l, "Str8"); + insertLongMap.put(10000000009l, "Str9"); + + assert longMap.size() == 1; + longMap.putAll(insertLongMap); + assert longMap.size() == 3; + assert longMap.get(10000000007l).equals("Str77"); + assert longMap.get(10000000008l).equals("Str8"); + assert longMap.get(10000000009l).equals("Str9"); + } + + @Test + public void mapValueConstructor_Test() { + LongMapImpl insertLongMap = new LongMapImpl<>(); + insertLongMap.put(10000000007l, "Str7"); + insertLongMap.put(10000000008l, "Str8"); + insertLongMap.put(10000000009l, "Str9"); + + longMap = new LongMapImpl<>(insertLongMap); + longMap.put(100000000010l, "Str10"); + + assert longMap.size() == 4; + assert longMap.get(10000000007l) == "Str7"; + assert longMap.get(10000000008l) == "Str8"; + assert longMap.get(10000000009l) == "Str9"; + assert longMap.get(100000000010l) == "Str10"; + } + + @Test + public void mapValueConstructorDuplicate_Test() { + LongMapImpl insertLongMap = new LongMapImpl<>(); + insertLongMap.put(10000000007l, "Str7"); + insertLongMap.put(10000000008l, "Str8"); + insertLongMap.put(10000000009l, "Str9"); + + longMap = new LongMapImpl<>(insertLongMap); + longMap.put(10000000009l, "Str99"); + + assert longMap.size() == 3; + assert longMap.get(10000000007l) == "Str7"; + assert longMap.get(10000000008l) == "Str8"; + assert longMap.get(10000000009l) == "Str99"; + } + + @Test + public void toString_Test() { + longMap.put(10000000001l, "Str"); + + String result = "{10000000001=Str}"; + + assert longMap.toString().equals(result); + } + + @Test + public void toStringEmpty_Test() { + assert longMap.toString().equals("{}"); + } + + @Test + public void clear_Test() { + longMap.clear(); + assert longMap.size() == 0; + } + + @Test + public void size_Test() { + assert longMap.size() == 0; + } + + @Test + public void isEmpty_Test() { + assert longMap.isEmpty(); + } +}