diff --git a/OcavaCore/src/main/java/com/ocadotechnology/config/ConfigParsers.java b/OcavaCore/src/main/java/com/ocadotechnology/config/ConfigParsers.java
index a51325c1..f395229d 100644
--- a/OcavaCore/src/main/java/com/ocadotechnology/config/ConfigParsers.java
+++ b/OcavaCore/src/main/java/com/ocadotechnology/config/ConfigParsers.java
@@ -244,7 +244,7 @@ public static double parseSpeed(String value, LengthUnit returnLengthUnit, TimeU
/**
* Parse the given String into an acceleration value in the given {@link LengthUnit} over {@link TimeUnit} squared.
- * The* String can specify input length and time units. If none are specified the SI Units are used. The parser can
+ * The String can specify input length and time units. If none are specified the SI Units are used. The parser can
* cope with case insensitivity, as well as adding an "S" to pluralise units.
*
*
diff --git a/OcavaCore/src/main/java/com/ocadotechnology/utils/SortByValueMap.java b/OcavaCore/src/main/java/com/ocadotechnology/utils/SortByValueMap.java
new file mode 100644
index 00000000..5ef70a83
--- /dev/null
+++ b/OcavaCore/src/main/java/com/ocadotechnology/utils/SortByValueMap.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright © 2017-2024 Ocado (Ocava)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ocadotechnology.utils;
+
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedMap;
+
+/**
+ * HashMap with iteration order defined by order of values. Sorts by values then keys.
+ * Non-equal keys must not compare as equal, as this will cause inconsistent ordering
+ * errors. This is only enforced lazily as active enforcement would incur a performance
+ * penalty.
+ *
+ * Note: Every view returned by this class is an immutable copy which will not reflect
+ * subsequent changes to the parent map. This has been done in order to make the
+ * development tractable.
+ */
+public class SortByValueMap implements SortedMap {
+ /**
+ * It is necessary to store the sequence and the mapping separately, since the sequence
+ * is dependent on the mapping. We could order entries instead of keys, but we still need
+ * to be able to query the value from the key in order to satisfy the map API.
+ */
+ private final Map map = new HashMap<>();
+ private final Comparator comparator;
+ private final SortedSet orderedKeys;
+
+ /**
+ * Creates a new instance of SortByValueMap with natural order comparators for both keys and
+ * values. Requires both classes to be Comparable.
+ */
+ public static , V extends Comparable> SortByValueMap createWithNaturalOrderComparators() {
+ return new SortByValueMap<>(Comparator.naturalOrder(), Comparator.naturalOrder());
+ }
+
+ /**
+ * Creates a new instance of SortByValueMap with a custom comparator for keys and natural order
+ * comparator for values. Requires values to be Comparable.
+ */
+ public static > SortByValueMap createWithNaturalOrderValueComparator(Comparator keyComparator) {
+ return new SortByValueMap<>(keyComparator, Comparator.naturalOrder());
+ }
+
+ /**
+ * Creates a new instance of SortByValueMap with a natural order comparator for keys and a custom
+ * comparator for values. Requires keys to be Comparable.
+ */
+ public static , V> SortByValueMap createWithNaturalOrderKeyComparator(Comparator valueComparator) {
+ return new SortByValueMap<>(Comparator.naturalOrder(), valueComparator);
+ }
+
+ /**
+ * Creates a new instance of SortByValueMap with custom comparators for both keys and values.
+ */
+ public static SortByValueMap createWithCustomComparators(Comparator keyComparator, Comparator valueComparator) {
+ return new SortByValueMap<>(keyComparator, valueComparator);
+ }
+
+ private SortByValueMap(Comparator super K> keyComparator, Comparator super V> valueComparator) {
+ this.comparator = buildComparator(keyComparator, valueComparator);
+ this.orderedKeys = new TreeSet<>(comparator);
+ }
+
+ private Comparator buildComparator(Comparator super K> keyComparator, Comparator super V> valueComparator) {
+ return (k1, k2) -> {
+ int result = valueComparator.compare(map.get(k1), map.get(k2));
+ if (result == 0) {
+ result = keyComparator.compare(k1, k2);
+ }
+ if (result == 0) {
+ Preconditions.checkState(k1.equals(k2), "Keys %s and %s are non-equal but return zero in their comparator", k1, k2);
+ }
+ return result;
+ };
+ }
+
+ @Override
+ public int size() {
+ return orderedKeys.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return orderedKeys.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return map.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ return map.containsValue(value);
+ }
+
+ @Override
+ public V get(Object key) {
+ return map.get(key);
+ }
+
+ @Override
+ public V put(K key, V value) {
+ clearSortOrderIfKnown(key);
+ V previous = map.put(key, value);
+ orderedKeys.add(key);
+ return previous;
+ }
+
+ @Override
+ public void putAll(Map extends K, ? extends V> m) {
+ for (K key : m.keySet()) {
+ clearSortOrderIfKnown(key);
+ }
+ map.putAll(m);
+ orderedKeys.addAll(m.keySet());
+ }
+
+ @Override
+ public V remove(Object key) {
+ clearSortOrderIfKnown(key);
+ return map.remove(key);
+ }
+
+ private void clearSortOrderIfKnown(Object key) {
+ if (map.containsKey(key)) {
+ orderedKeys.remove(key);
+ }
+ }
+
+ @Override
+ public void clear() {
+ orderedKeys.clear();
+ map.clear();
+ }
+
+ /**
+ * Returns an immutable copy of the key set, with iteration order defined by the
+ * iteration order of the map. The set is a snapshot of the keys at the time of
+ * the call, and will not reflect subsequent changes to the map.
+ */
+ @Override
+ @Nonnull
+ public ImmutableSet keySet() {
+ return ImmutableSet.copyOf(orderedKeys);
+ }
+
+ /**
+ * Returns an immutable copy of the value collection, with iteration order defined by the
+ * iteration order of the map. The collection is a snapshot of the values at the time of
+ * the call, and will not reflect subsequent changes to the map.
+ */
+ @Override
+ @Nonnull
+ public ImmutableList values() {
+ return orderedKeys.stream().map(map::get).collect(ImmutableList.toImmutableList());
+ }
+
+ /**
+ * Returns an immutable copy of the entry set, with iteration order defined by the
+ * iteration order of the map. The set is a snapshot of the entries at the time of
+ * the call, and will not reflect subsequent changes to the map.
+ */
+ @Override
+ @Nonnull
+ public ImmutableSet> entrySet() {
+ return orderedKeys.stream().map(k -> new SimpleImmutableEntry<>(k, map.get(k))).collect(ImmutableSet.toImmutableSet());
+ }
+
+ @Override
+ public K firstKey() {
+ return orderedKeys.first();
+ }
+
+ @Override
+ public K lastKey() {
+ return orderedKeys.last();
+ }
+
+ /**
+ * Returns an immutable copy of the portion of this map whose keys range from fromKey to toKey.
+ * Iteration order of the returned copy will match the parent map. The map is a snapshot of the
+ * entries at the time of the call, and will not reflect subsequent changes to the map.
+ */
+ public ImmutableSortedMap subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {
+ ImmutableSortedMap.Builder builder = ImmutableSortedMap.orderedBy(comparator);
+ // orderedKeys.subSet is fromInclusive/toExclusive by default, so I need to potentially remove the first key and
+ // add the last key depending on the values provided by the caller.
+ for (K k : orderedKeys.subSet(fromKey, toKey)) {
+ if (!fromInclusive && k.equals(fromKey)) {
+ continue;
+ }
+ builder.put(k, map.get(k));
+ }
+ if (toInclusive && map.containsKey(toKey)) {
+ builder.put(toKey, map.get(toKey));
+ }
+ return builder.build();
+ }
+ /**
+ * Returns an immutable copy of the portion of this map whose keys range from fromKey inclusive
+ * to toKey exclusive. Iteration order of the returned copy will match the parent map. The map
+ * is a snapshot of the entries at the time of the call, and will not reflect subsequent changes
+ * to the map.
+ */
+ @Override
+ @Nonnull
+ public ImmutableSortedMap subMap(K fromKey, K toKey) {
+ return subMap(fromKey, true, toKey, false);
+ }
+
+ /**
+ * Returns an immutable copy of the portion of this map whose keys are less than (or equal to if
+ * inclusive is set to true) toKey. Iteration order of the returned copy will match the parent map.
+ * The map is a snapshot of the entries at the time of the call, and will not reflect subsequent
+ * changes to the map.
+ */
+ public ImmutableSortedMap headMap(K toKey, boolean inclusive) {
+ ImmutableSortedMap.Builder builder = ImmutableSortedMap.orderedBy(comparator);
+ orderedKeys.headSet(toKey).forEach(k -> builder.put(k, map.get(k)));
+ if (inclusive && map.containsKey(toKey)) {
+ builder.put(toKey, map.get(toKey));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns an immutable copy of the portion of this map whose keys are strictly less than toKey.
+ * Iteration order of the returned copy will match the parent map. The map is a snapshot of the
+ * entries at the time of the call, and will not reflect subsequent changes to the map.
+ */
+ @Override
+ @Nonnull
+ public ImmutableSortedMap headMap(K toKey) {
+ return headMap(toKey, false);
+ }
+
+ /**
+ * Returns an immutable copy of the portion of this map whose keys are greater than (or equal to if
+ * inclusive is set to true) fromKey. Iteration order of the returned copy will match the parent map.
+ * The map is a snapshot of the entries at the time of the call, and will not reflect subsequent
+ * changes to the map.
+ */
+ public ImmutableSortedMap tailMap(K fromKey, boolean inclusive) {
+ ImmutableSortedMap.Builder builder = ImmutableSortedMap.orderedBy(comparator);
+ for (K k : orderedKeys.tailSet(fromKey)) {
+ if (inclusive || !k.equals(fromKey)) {
+ builder.put(k, map.get(k));
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns an immutable copy of the portion of this map whose keys are greater than or equal to fromKey.
+ * Iteration order of the returned copy will match the parent map. The map is a snapshot of the entries
+ * at the time of the call, and will not reflect subsequent changes to the map.
+ */
+ @Override
+ @Nonnull
+ public ImmutableSortedMap tailMap(K fromKey) {
+ return tailMap(fromKey, true);
+ }
+
+ @Override
+ public Comparator super K> comparator() {
+ return orderedKeys.comparator();
+ }
+}
diff --git a/OcavaCore/src/test/java/com/ocadotechnology/utils/SortByValueMapTest.java b/OcavaCore/src/test/java/com/ocadotechnology/utils/SortByValueMapTest.java
new file mode 100644
index 00000000..930b1abc
--- /dev/null
+++ b/OcavaCore/src/test/java/com/ocadotechnology/utils/SortByValueMapTest.java
@@ -0,0 +1,408 @@
+/*
+ * Copyright © 2017-2024 Ocado (Ocava)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ocadotechnology.utils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.ocadotechnology.wrappers.Pair;
+
+class SortByValueMapTest {
+ private static final String KEY_1 = "Alpha";
+ private static final String KEY_2 = "Bravo";
+ private static final String KEY_3 = "Charlie";
+ private final SortByValueMap testMap = SortByValueMap.createWithNaturalOrderComparators();
+
+ @Test
+ void put_whenInconsistentEquals_thenThrowsException() {
+ SortByValueMap clashingTestMap = SortByValueMap.createWithNaturalOrderValueComparator((s1, s2) -> 0);
+ clashingTestMap.put(KEY_1, 1);
+ assertThatThrownBy(() -> clashingTestMap.put(KEY_2, 1)).isInstanceOf(IllegalStateException.class);
+ }
+
+ /*
+ * We could consider how to proactively detect non-equal keys with an indeterminate sort order, but for now, these
+ * are only detected when the values are the same. This is because any proactive detection would incur a performance
+ * penalty which is not obviously justified.
+ */
+ @Test
+ void put_whenInconsistentEqualsButDifferentValues_thenDoesNotThrow() {
+ SortByValueMap clashingTestMap = SortByValueMap.createWithNaturalOrderValueComparator((s1, s2) -> 0);
+ clashingTestMap.put(KEY_1, 1);
+ clashingTestMap.put(KEY_2, 2);
+
+ assertThat(clashingTestMap.get(KEY_1)).isEqualTo(1);
+ assertThat(clashingTestMap.get(KEY_2)).isEqualTo(2);
+ }
+
+ @Test
+ void get_whenMappingIsPresent_returnsValue() {
+ testMap.put(KEY_1, 1);
+
+ assertThat(testMap.get(KEY_1)).isEqualTo(1);
+ }
+
+ @Test
+ void get_whenMappingIsAbsent_returnsNull() {
+ testMap.put(KEY_1, 1);
+
+ assertThat(testMap.get(KEY_2)).isNull();
+ }
+
+ @Test
+ void get_whenMultipleMappingsPresent_returnsValues() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+
+ assertThat(testMap.get(KEY_1)).isEqualTo(1);
+ assertThat(testMap.get(KEY_2)).isEqualTo(2);
+ }
+
+ @Test
+ void get_whenDuplicateValuesPresent_returnsValues() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 1);
+
+ assertThat(testMap.get(KEY_1)).isEqualTo(1);
+ assertThat(testMap.get(KEY_2)).isEqualTo(1);
+ }
+
+ @Test
+ void remove_whenKeyExists_returnsValue() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+
+ assertThat(testMap.remove(KEY_1)).isEqualTo(1);
+ assertThat(testMap.get(KEY_1)).isNull();
+ }
+
+ @Test
+ void remove_whenKeyDoesNotExist_returnsNull() {
+ testMap.put(KEY_1, 1);
+
+ assertThat(testMap.remove(KEY_2)).isNull();
+ }
+
+ @Test
+ void putAll_whenKeysNotPresent_valuesAdded() {
+ Map newMap = ImmutableMap.of(KEY_1, 1, KEY_2, 2);
+ testMap.putAll(newMap);
+
+ assertThat(testMap.get(KEY_1)).isEqualTo(1);
+ assertThat(testMap.get(KEY_2)).isEqualTo(2);
+ }
+
+ @Test
+ void putAll_whenKeysPresent_valuesReplaced() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+
+ Map newMap = ImmutableMap.of(KEY_1, 2, KEY_2, 1);
+ testMap.putAll(newMap);
+
+ assertThat(testMap.get(KEY_1)).isEqualTo(2);
+ assertThat(testMap.get(KEY_2)).isEqualTo(1);
+ }
+
+ @Test
+ void forEach_whenValuesDistinct_respectsSortOrder() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+
+ List> invocations = new ArrayList<>();
+ testMap.forEach((k, v) -> invocations.add(Pair.of(k, v)));
+ assertThat(invocations).containsExactly(Pair.of(KEY_1, 1), Pair.of(KEY_2, 2));
+
+ testMap.put(KEY_1, 2);
+ testMap.put(KEY_2, 1);
+
+ invocations.clear();
+ testMap.forEach((k, v) -> invocations.add(Pair.of(k, v)));
+ assertThat(invocations).containsExactly(Pair.of(KEY_2, 1), Pair.of(KEY_1, 2));
+ }
+
+ @Test
+ void forEach_whenValuesEqual_respectsKeySortOrder() {
+ testMap.put(KEY_2, 1);
+ testMap.put(KEY_1, 1);
+
+ List> invocations = new ArrayList<>();
+ testMap.forEach((k, v) -> invocations.add(Pair.of(k, v)));
+ assertThat(invocations).containsExactly(Pair.of(KEY_1, 1), Pair.of(KEY_2, 1));
+ }
+
+ @Test
+ void firstKey_whenEmpty_throwsException() {
+ assertThatThrownBy(testMap::firstKey).isInstanceOf(NoSuchElementException.class);
+ }
+
+ @Test
+ void firstKey_whenNotEmpty_returnsFirstKeyByValueOrdering() {
+ testMap.put(KEY_1, 2);
+ testMap.put(KEY_2, 1);
+
+ assertThat(testMap.firstKey()).isEqualTo(KEY_2);
+ }
+
+ @Test
+ void firstKey_whenNotEmpty_returnsFirstKeyByKeyOrdering() {
+ testMap.put(KEY_2, 1);
+ testMap.put(KEY_1, 1);
+
+ assertThat(testMap.firstKey()).isEqualTo(KEY_1);
+ }
+
+ @Test
+ void lastKey_whenEmpty_throwsException() {
+ assertThatThrownBy(testMap::lastKey).isInstanceOf(NoSuchElementException.class);
+ }
+
+ @Test
+ void lastKey_whenNotEmpty_returnsLastKeyByValueOrdering() {
+ testMap.put(KEY_1, 2);
+ testMap.put(KEY_2, 1);
+
+ assertThat(testMap.lastKey()).isEqualTo(KEY_1);
+ }
+
+ @Test
+ void lastKey_whenNotEmpty_returnsLastKeyByKeyOrdering() {
+ testMap.put(KEY_2, 1);
+ testMap.put(KEY_1, 1);
+
+ assertThat(testMap.lastKey()).isEqualTo(KEY_2);
+ }
+
+ @Test
+ void subMap_whenKeyNotInMap_throwsException() {
+ assertThatThrownBy(() -> testMap.subMap(KEY_1, KEY_2)).isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void subMap_whenFromKeyIsGreaterThanToKey_throwsException() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+
+ assertThatThrownBy(() -> testMap.subMap(KEY_2, KEY_1)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void subMap_whenFromKeyIsEqualToToKey_returnsEmptyMap() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+
+ assertThat(testMap.subMap(KEY_1, KEY_1)).isEmpty();
+ }
+
+ @Test
+ void subMap_whenFromKeyIsLessThanToKey_returnsMapWithKeysInRange() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+ testMap.put(KEY_3, 3);
+
+ SimpleImmutableEntry firstEntry = new SimpleImmutableEntry<>(KEY_1, 1);
+ SimpleImmutableEntry secondEntry = new SimpleImmutableEntry<>(KEY_2, 2);
+
+ assertThat(testMap.subMap(KEY_1, KEY_2).entrySet()).containsExactly(firstEntry);
+ assertThat(testMap.subMap(KEY_1, KEY_3).entrySet()).containsExactly(firstEntry, secondEntry);
+ assertThat(testMap.subMap(KEY_2, KEY_3).entrySet()).containsExactly(secondEntry);
+ }
+
+ @Test
+ void subMap_whenFromInclusiveFalse_excludesFromKey() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+ testMap.put(KEY_3, 3);
+
+ SimpleImmutableEntry secondEntry = new SimpleImmutableEntry<>(KEY_2, 2);
+
+ assertThat(testMap.subMap(KEY_1, false, KEY_2, false).entrySet()).isEmpty();
+ assertThat(testMap.subMap(KEY_1, false, KEY_3, false).entrySet()).containsExactly(secondEntry);
+ assertThat(testMap.subMap(KEY_2, false, KEY_3, false).entrySet()).isEmpty();
+ }
+
+ @Test
+ void subMap_whenToInclusiveTrue_includesToKey() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+ testMap.put(KEY_3, 3);
+
+ SimpleImmutableEntry firstEntry = new SimpleImmutableEntry<>(KEY_1, 1);
+ SimpleImmutableEntry secondEntry = new SimpleImmutableEntry<>(KEY_2, 2);
+ SimpleImmutableEntry thirdEntry = new SimpleImmutableEntry<>(KEY_3, 3);
+
+ assertThat(testMap.subMap(KEY_1, true, KEY_2, true).entrySet()).containsExactly(firstEntry, secondEntry);
+ assertThat(testMap.subMap(KEY_1, true, KEY_3, true).entrySet()).containsExactly(firstEntry, secondEntry, thirdEntry);
+ assertThat(testMap.subMap(KEY_2, true, KEY_3, true).entrySet()).containsExactly(secondEntry, thirdEntry);
+ }
+
+ @Test
+ void compute_whenKeyNotInMap_returnsNewValue() {
+ testMap.put(KEY_1, 1);
+
+ assertThat(testMap.compute(KEY_2, (k, v) -> {
+ assertThat(v).isNull();
+ return 2;
+ })).isEqualTo(2);
+ assertThat(testMap.get(KEY_2)).isEqualTo(2);
+ }
+
+ @Test
+ void compute_whenKeyInMap_returnsNewValue() {
+ testMap.put(KEY_1, 1);
+
+ assertThat(testMap.compute(KEY_1, (k, v) -> {
+ assertThat(v).isEqualTo(1);
+ return 2;
+ })).isEqualTo(2);
+ assertThat(testMap.get(KEY_1)).isEqualTo(2);
+ }
+
+ @Test
+ void computeIfAbsent_whenKeyNotInMap_returnsNewValue() {
+ testMap.put(KEY_1, 1);
+
+ assertThat(testMap.computeIfAbsent(KEY_2, k -> 2)).isEqualTo(2);
+ assertThat(testMap.get(KEY_2)).isEqualTo(2);
+ }
+
+ @Test
+ void computeIfAbsent_whenKeyInMap_returnsExistingValue() {
+ testMap.put(KEY_1, 1);
+
+ assertThat(testMap.computeIfAbsent(KEY_1, k -> {
+ Assertions.fail();
+ return -1;
+ })).isEqualTo(1);
+ assertThat(testMap.get(KEY_1)).isEqualTo(1);
+ }
+
+ @Test
+ void computeIfPresent_whenKeyNotInMap_returnsNull() {
+ testMap.put(KEY_1, 1);
+
+ assertThat(testMap.computeIfPresent(KEY_2, (k, v) -> {
+ Assertions.fail();
+ return -1;
+ })).isNull();
+ }
+
+ @Test
+ void computeIfPresent_whenKeyInMap_returnsNewValue() {
+ testMap.put(KEY_1, 1);
+
+ assertThat(testMap.computeIfPresent(KEY_1, (k, v) -> {
+ assertThat(v).isEqualTo(1);
+ return 2;
+ })).isEqualTo(2);
+ assertThat(testMap.get(KEY_1)).isEqualTo(2);
+ }
+
+ @Test
+ void headMap_whenKeyNotInMap_throwsException() {
+ assertThatThrownBy(() -> testMap.headMap(KEY_1)).isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void headMap_whenKeyIsFirstKey_returnsEmptyMap() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+
+ assertThat(testMap.headMap(KEY_1).entrySet()).isEmpty();
+ }
+
+ @Test
+ void headMap_whenKeyIsNotFirstKey_returnsMapWithKeysLessThanKey() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+ testMap.put(KEY_3, 3);
+
+ SimpleImmutableEntry firstEntry = new SimpleImmutableEntry<>(KEY_1, 1);
+ SimpleImmutableEntry secondEntry = new SimpleImmutableEntry<>(KEY_2, 2);
+
+ assertThat(testMap.headMap(KEY_2).entrySet()).containsExactly(firstEntry);
+ assertThat(testMap.headMap(KEY_3).entrySet()).containsExactly(firstEntry, secondEntry);
+ }
+
+ @Test
+ void headMap_whenInclusiveTrue_includesKey() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+ testMap.put(KEY_3, 3);
+
+ SimpleImmutableEntry firstEntry = new SimpleImmutableEntry<>(KEY_1, 1);
+ SimpleImmutableEntry secondEntry = new SimpleImmutableEntry<>(KEY_2, 2);
+ SimpleImmutableEntry thirdEntry = new SimpleImmutableEntry<>(KEY_3, 3);
+
+ assertThat(testMap.headMap(KEY_1, true).entrySet()).containsExactly(firstEntry);
+ assertThat(testMap.headMap(KEY_2, true).entrySet()).containsExactly(firstEntry, secondEntry);
+ assertThat(testMap.headMap(KEY_3, true).entrySet()).containsExactly(firstEntry, secondEntry, thirdEntry);
+ }
+
+ @Test
+ void tailMap_whenKeyNotInMap_throwsException() {
+ assertThatThrownBy(() -> testMap.tailMap(KEY_1)).isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void tailMap_whenKeyIsLastKey_returnsSingleEntryMap() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+
+ SimpleImmutableEntry secondEntry = new SimpleImmutableEntry<>(KEY_2, 2);
+
+ assertThat(testMap.tailMap(KEY_2).entrySet()).containsExactly(secondEntry);
+ }
+
+ @Test
+ void tailMap_whenKeyIsNotLastKey_returnsMapWithKeysGreaterThanOrEqualKey() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+ testMap.put(KEY_3, 3);
+
+ SimpleImmutableEntry firstEntry = new SimpleImmutableEntry<>(KEY_1, 1);
+ SimpleImmutableEntry secondEntry = new SimpleImmutableEntry<>(KEY_2, 2);
+ SimpleImmutableEntry thirdEntry = new SimpleImmutableEntry<>(KEY_3, 3);
+
+ assertThat(testMap.tailMap(KEY_1).entrySet()).containsExactly(firstEntry, secondEntry, thirdEntry);
+ assertThat(testMap.tailMap(KEY_2).entrySet()).containsExactly(secondEntry, thirdEntry);
+ assertThat(testMap.tailMap(KEY_3).entrySet()).containsExactly(thirdEntry);
+ }
+
+ @Test
+ void tailMap_whenInclusiveFalse_excludesKey() {
+ testMap.put(KEY_1, 1);
+ testMap.put(KEY_2, 2);
+ testMap.put(KEY_3, 3);
+
+ SimpleImmutableEntry secondEntry = new SimpleImmutableEntry<>(KEY_2, 2);
+ SimpleImmutableEntry thirdEntry = new SimpleImmutableEntry<>(KEY_3, 3);
+
+ assertThat(testMap.tailMap(KEY_1, false).entrySet()).containsExactly(secondEntry, thirdEntry);
+ assertThat(testMap.tailMap(KEY_2, false).entrySet()).containsExactly(thirdEntry);
+ assertThat(testMap.tailMap(KEY_3, false).entrySet()).isEmpty();
+ }
+}
\ No newline at end of file