diff --git a/src/main/java/ch/njol/skript/expressions/ExprAmount.java b/src/main/java/ch/njol/skript/expressions/ExprAmount.java index 2a8d54c03e5..3290746c61b 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprAmount.java +++ b/src/main/java/ch/njol/skript/expressions/ExprAmount.java @@ -11,6 +11,7 @@ import ch.njol.skript.lang.ExpressionList; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.Variable; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.skript.lang.util.common.AnyAmount; import ch.njol.skript.util.LiteralUtils; @@ -42,6 +43,7 @@ public class ExprAmount extends SimpleExpression { @SuppressWarnings("null") private ExpressionList exprs; private @Nullable Expression any; + private @Nullable Variable list; @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { @@ -54,7 +56,6 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye this.exprs = exprs[0] instanceof ExpressionList exprList ? exprList : new ExpressionList<>(new Expression[]{ exprs[0] }, Object.class, false); - this.exprs = (ExpressionList) LiteralUtils.defendExpression(this.exprs); if (!LiteralUtils.canInitSafely(this.exprs)) { return false; @@ -65,6 +66,9 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye return false; } + if (exprs[0] instanceof Variable variable) + this.list = variable; + return true; } @@ -72,6 +76,10 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye protected Number[] get(Event event) { if (any != null) return new Number[] {any.getOptionalSingle(event).orElse(() -> 0).amount()}; + + if (list != null) + return new Long[]{(long) list.size(event)}; + return new Long[]{(long) exprs.getArray(event).length}; } diff --git a/src/main/java/ch/njol/skript/lang/Variable.java b/src/main/java/ch/njol/skript/lang/Variable.java index ef437d93040..dc4fc40cc43 100644 --- a/src/main/java/ch/njol/skript/lang/Variable.java +++ b/src/main/java/ch/njol/skript/lang/Variable.java @@ -14,6 +14,8 @@ import ch.njol.skript.structures.StructVariables.DefaultVariables; import ch.njol.skript.util.StringMode; import ch.njol.skript.util.Utils; +import com.google.common.base.Preconditions; +import org.skriptlang.skript.util.IndexTrackingTreeMap; import ch.njol.skript.variables.Variables; import ch.njol.util.Kleenean; import ch.njol.util.Pair; @@ -446,7 +448,7 @@ private T[] getConvertedArray(Event event) { } private void set(Event event, @Nullable Object value) { - Variables.setVariable("" + name.toString(event), value, event, local); + Variables.setVariable(name.toString(event), value, event, local); } private void setIndex(Event event, String index, @Nullable Object value) { @@ -456,6 +458,33 @@ private void setIndex(Event event, String index, @Nullable Object value) { Variables.setVariable(name.substring(0, name.length() - 1) + index, value, event, local); } + public int size(Event event) { + Preconditions.checkState(list, "Cannot get the size of a single variable"); + Map map = (Map) getRaw(event); + if (map == null) + return 0; + + int size = map.size(); + if (map.containsKey(null)) // if we're trying to get the size of {_list::*}, exclude {_list} from being counted + size--; + + if (!(map instanceof IndexTrackingTreeMap indexTrackingMap)) { + for (Object value : map.values()) { + if (value instanceof Map sublist && !sublist.containsKey(null)) + size--; + } + return size; + } + + Collection sublistIndices = indexTrackingMap.mapIndices(); + for (String sublistIndex : sublistIndices) { + if (!((Map) map.get(sublistIndex)).containsKey(null)) + size--; + } + + return size; + } + @Override public Class @Nullable [] acceptChange(ChangeMode mode) { if (!list && mode == ChangeMode.SET) @@ -494,22 +523,6 @@ public void change(Event event, Object @NotNull [] delta, ChangeMode mode, @NotN public void change(Event event, Object @Nullable [] delta, ChangeMode mode) throws UnsupportedOperationException { switch (mode) { case DELETE: - if (list) { - ArrayList toDelete = new ArrayList<>(); - Map map = (Map) getRaw(event); - if (map == null) - return; - for (Entry entry : map.entrySet()) { - if (entry.getKey() != null){ - toDelete.add(entry.getKey()); - } - } - for (String index : toDelete) { - assert index != null; - setIndex(event, index, null); - } - } - set(event, null); break; case SET: @@ -594,6 +607,13 @@ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) thro } } else { assert mode == ChangeMode.ADD; + if (map instanceof IndexTrackingTreeMap indexTrackingMap) { + for (Object value : delta) { + int index = indexTrackingMap.nextOpenIndex(); + setIndex(event, String.valueOf(index), value); + } + return; + } int i = 1; for (Object value : delta) { if (map != null) diff --git a/src/main/java/ch/njol/skript/variables/VariablesMap.java b/src/main/java/ch/njol/skript/variables/VariablesMap.java index 9934f46eba5..bc366e7b44f 100644 --- a/src/main/java/ch/njol/skript/variables/VariablesMap.java +++ b/src/main/java/ch/njol/skript/variables/VariablesMap.java @@ -3,6 +3,7 @@ import ch.njol.skript.lang.Variable; import ch.njol.util.StringUtils; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.util.IndexTrackingTreeMap; import java.util.Comparator; import java.util.HashMap; @@ -216,7 +217,7 @@ void setVariable(String name, @Nullable Object value) { break; } else if (value != null) { // Create child node, add it to parent and continue iteration - childNode = new TreeMap<>(VARIABLE_NAME_COMPARATOR); + childNode = new IndexTrackingTreeMap<>(VARIABLE_NAME_COMPARATOR); parent.put(childNodeName, childNode); parent = (TreeMap) childNode; @@ -269,7 +270,7 @@ void setVariable(String name, @Nullable Object value) { break; } else if (value != null) { // Need to continue iteration, create new child node and put old value in it - TreeMap newChildNodeMap = new TreeMap<>(VARIABLE_NAME_COMPARATOR); + TreeMap newChildNodeMap = new IndexTrackingTreeMap<>(VARIABLE_NAME_COMPARATOR); newChildNodeMap.put(null, childNode); // Add new child node to parent @@ -334,7 +335,7 @@ public VariablesMap copy() { */ @SuppressWarnings("unchecked") private static TreeMap copyTreeMap(TreeMap original) { - TreeMap copy = new TreeMap<>(VARIABLE_NAME_COMPARATOR); + TreeMap copy = new IndexTrackingTreeMap<>(VARIABLE_NAME_COMPARATOR); for (Entry child : original.entrySet()) { String key = child.getKey(); diff --git a/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprAmount.java b/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprAmount.java index beb93f20f12..7a7567967d6 100644 --- a/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprAmount.java +++ b/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprAmount.java @@ -6,6 +6,7 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionList; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.Variable; import ch.njol.skript.util.LiteralUtils; import ch.njol.util.Kleenean; import org.bukkit.event.Event; @@ -40,6 +41,7 @@ public static void register(SyntaxRegistry registry, Origin origin) { } private ExpressionList exprs; + private @Nullable Variable list; private boolean useProperties; @Override @@ -48,13 +50,14 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is // amounts of x, y -> property // amount of x, y -> list length useProperties = parseResult.hasTag("s") || expressions[0].isSingle(); - if (useProperties) { + if (useProperties) return super.init(expressions, matchedPattern, isDelayed, parseResult); - } else { - // if exprlist or varlist, count elements - this.exprs = asExprList(expressions[0]); - return LiteralUtils.canInitSafely(this.exprs); - } + + // if exprlist or varlist, count elements + this.exprs = asExprList(expressions[0]); + if (expressions[0] instanceof Variable variable) + this.list = variable; + return LiteralUtils.canInitSafely(this.exprs); } /** @@ -78,6 +81,10 @@ public static ExpressionList asExprList(Expression expr) { protected Object @Nullable [] get(Event event) { if (useProperties) return super.get(event); + + if (list != null) + return new Long[]{(long) list.size(event)}; + return new Long[]{(long) exprs.getArray(event).length}; } diff --git a/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprNumber.java b/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprNumber.java index 7802e8f48ae..a36377c2042 100644 --- a/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprNumber.java +++ b/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprNumber.java @@ -6,6 +6,7 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionList; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.Variable; import ch.njol.skript.util.LiteralUtils; import ch.njol.util.Kleenean; import org.bukkit.event.Event; @@ -37,27 +38,33 @@ public static void register(SyntaxRegistry registry, Origin origin) { } private ExpressionList exprs; + private @Nullable Variable list; private boolean useProperties; @Override public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - // size[s] of x -> property - // sizes of x, y -> property - // size of x, y -> list length + // number[s] of x -> property + // numbers of x, y -> property + // number of x, y -> list length useProperties = parseResult.hasTag("s") || expressions[0].isSingle(); - if (useProperties) { + if (useProperties) return super.init(expressions, matchedPattern, isDelayed, parseResult); - } else { - // if exprlist or varlist, count elements - this.exprs = PropExprAmount.asExprList(expressions[0]); - return LiteralUtils.canInitSafely(this.exprs); - } + + // if exprlist or varlist, count elements + this.exprs = PropExprAmount.asExprList(expressions[0]); + if (expressions[0] instanceof Variable variable) + this.list = variable; + return LiteralUtils.canInitSafely(this.exprs); } @Override protected Object @Nullable [] get(Event event) { if (useProperties) return super.get(event); + + if (list != null) + return new Long[]{(long) list.size(event)}; + return new Long[]{(long) exprs.getArray(event).length}; } diff --git a/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprSize.java b/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprSize.java index 10ccf726f3b..00c1b3cd8bc 100644 --- a/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprSize.java +++ b/src/main/java/org/skriptlang/skript/common/properties/expressions/PropExprSize.java @@ -6,6 +6,7 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionList; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.Variable; import ch.njol.skript.util.LiteralUtils; import ch.njol.util.Kleenean; import org.bukkit.event.Event; @@ -37,6 +38,7 @@ public static void register(SyntaxRegistry registry, Origin origin) { } private ExpressionList exprs; + private @Nullable Variable list; private boolean useProperties; @Override @@ -45,19 +47,24 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is // sizes of x, y -> property // size of x, y -> list length useProperties = parseResult.hasTag("s") || expressions[0].isSingle(); - if (useProperties) { + if (useProperties) return super.init(expressions, matchedPattern, isDelayed, parseResult); - } else { - // if exprlist or varlist, count elements - this.exprs = PropExprAmount.asExprList(expressions[0]); - return LiteralUtils.canInitSafely(this.exprs); - } + + // if exprlist or varlist, count elements + this.exprs = PropExprAmount.asExprList(expressions[0]); + if (expressions[0] instanceof Variable variable) + this.list = variable; + return LiteralUtils.canInitSafely(this.exprs); } @Override protected Object @Nullable [] get(Event event) { if (useProperties) return super.get(event); + + if (list != null) + return new Long[]{(long) list.size(event)}; + return new Long[]{(long) exprs.getArray(event).length}; } diff --git a/src/main/java/org/skriptlang/skript/util/IndexTrackingTreeMap.java b/src/main/java/org/skriptlang/skript/util/IndexTrackingTreeMap.java new file mode 100644 index 00000000000..f480d580a95 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/IndexTrackingTreeMap.java @@ -0,0 +1,179 @@ +package org.skriptlang.skript.util; + +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.*; + +/** + * A {@link TreeMap} that supports automatically assigning the next available + * positive integer key, represented as a string. + * + *

In addition to arbitrary string keys, this map can be used with + * positive integer string keys such as {@code "1"}, {@code "2"}, and + * {@code "3"}. The {@link #add(Object)} method inserts a value using the + * next available integer key.

+ * + * @param the type of mapped values + */ +public class IndexTrackingTreeMap extends TreeMap { + + private final Set mapIndices = new HashSet<>(); + + private final Set numericalIndices = new HashSet<>(); + private int nextIndex = 1; + private int maxIndex = -1; + + public IndexTrackingTreeMap() { + super(); + } + + public IndexTrackingTreeMap(Comparator comparator) { + super(comparator); + } + + @Override + public V put(String key, V value) { + V previous = super.put(key, value); + + if (previous == null && value != null) { + handleInsert(key, parsePositiveInt(key), value); + } else if (previous != null && value == null) { + handleRemove(key, previous); + } else if (previous != null) { + handleReplace(key, previous, value); + } + + return previous; + } + + /** + * Adds the given value under the first available positive integer key. + * + * @param value the value to add, cannot be null + */ + public void add(V value) { + Preconditions.checkNotNull(value, "value"); + String key = String.valueOf(nextIndex); + + super.put(key, value); + handleInsert(key, nextIndex, value); + } + + @Override + public V remove(Object key) { + V value = super.remove(key); + if (value != null && key instanceof String index) + handleRemove(index, value); + return value; + } + + @Override + public void clear() { + super.clear(); + numericalIndices.clear(); + mapIndices.clear(); + nextIndex = 1; + maxIndex = -1; + } + + /** + * Finds the first available positive integer index that is not currently + * used as a key in this map. + * + *

This method inspects tracked numeric keys and returns the smallest + * missing index, starting at {@code 1}.

+ * + * @return the next available positive integer index + */ + public int nextOpenIndex() { + return nextIndex; + } + + public boolean consecutive() { + return nextIndex == maxIndex + 1; + } + + /** + * Returns an unmodifiable view of the keys that map to other {@link Map} instances. + * + * @return a collection of all keys pointing to a map + */ + public @UnmodifiableView Collection mapIndices() { + return Collections.unmodifiableCollection(mapIndices); + } + + private void handleInsert(String key, int index, V value) { + if (value instanceof Map) + mapIndices.add(key); + + if (index < 0) + return; + + numericalIndices.add(index); + + maxIndex = Math.max(maxIndex, index); + advanceNextIndex(); + } + + private void handleReplace(String key, V previous, V value) { + if (value instanceof Map) { + mapIndices.add(key); + } else if (previous instanceof Map) { + mapIndices.remove(key); + } + } + + private void handleRemove(String key, V previous) { + if (previous instanceof Map) + mapIndices.remove(key); + + int index = parsePositiveInt(key); + if (index < 0) + return; + + numericalIndices.remove(index); + + if (index == maxIndex) + recomputeMaxIndex(); + nextIndex = Math.min(nextIndex, index); + } + + private void advanceNextIndex() { + if (nextIndex == maxIndex) { + nextIndex++; + return; + } + while (numericalIndices.contains(nextIndex)) + nextIndex++; + } + + private void recomputeMaxIndex() { + while (maxIndex >= 0 && !numericalIndices.contains(maxIndex)) + maxIndex--; + } + + private int parsePositiveInt(String string) { + if (string == null || string.isBlank() || string.charAt(0) == '0') // Don't handle leading-zero integers + return -1; + + int value = 0; + try { + for (int i = 0; i < string.length(); i++) { + char c = string.charAt(i); + if (!isDigit(c)) + return -1; + value = Math.addExact(value * 10, c - '0'); + } + } catch (ArithmeticException e) { // overflow + return -1; + } + + return value; + } + + private boolean isDigit(int codepoint) { + return codepoint >= '0' && codepoint <= '9'; + } + +} diff --git a/src/test/java/org/skriptlang/skript/util/IndexTrackingTreeMapTest.java b/src/test/java/org/skriptlang/skript/util/IndexTrackingTreeMapTest.java new file mode 100644 index 00000000000..488ed049a00 --- /dev/null +++ b/src/test/java/org/skriptlang/skript/util/IndexTrackingTreeMapTest.java @@ -0,0 +1,303 @@ +package org.skriptlang.skript.util; + +import org.junit.Test; + +import java.util.Collection; +import java.util.Map; + +import static org.junit.Assert.*; + +@SuppressWarnings("OverwrittenKey") +public class IndexTrackingTreeMapTest { + + private IndexTrackingTreeMap newMap() { + return new IndexTrackingTreeMap<>(); + } + + @Test + public void nextOpenIndexReturnsOneWhenMapIsEmpty() { + IndexTrackingTreeMap map = newMap(); + + assertEquals(1, map.nextOpenIndex()); + } + + @Test + public void addUsesKeyOneWhenMapIsEmpty() { + IndexTrackingTreeMap map = newMap(); + + map.add("value"); + + assertEquals("value", map.get("1")); + assertEquals(1, map.size()); + assertEquals(2, map.nextOpenIndex()); + } + + @Test + public void nextOpenIndexReturnsOneWhenFirstNumericKeyIsMissing() { + IndexTrackingTreeMap map = newMap(); + map.put("2", "two"); + map.put("3", "three"); + + assertEquals(1, map.nextOpenIndex()); + } + + @Test + public void nextOpenIndexReturnsNextValueForConsecutiveKeys() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("2", "two"); + map.put("3", "three"); + + assertEquals(4, map.nextOpenIndex()); + } + + @Test + public void nextOpenIndexReturnsFirstGapInMiddle() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("2", "two"); + map.put("4", "four"); + + assertEquals(3, map.nextOpenIndex()); + } + + @Test + public void nextOpenIndexReturnsFirstGapWhenMultipleGapsExist() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("3", "three"); + map.put("5", "five"); + + assertEquals(2, map.nextOpenIndex()); + } + + @Test + public void nonNumericKeysDoNotAffectNextOpenIndex() { + IndexTrackingTreeMap map = newMap(); + map.put("foo", "foo"); + map.put("bar", "bar"); + + assertEquals(1, map.nextOpenIndex()); + } + + @Test + public void mixedNumericAndNonNumericKeysOnlyTrackNumericOnes() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("foo", "foo"); + map.put("3", "three"); + map.put("bar", "bar"); + + assertEquals(2, map.nextOpenIndex()); + } + + @Test + public void overwriteExistingNumericKeyDoesNotDuplicateTracking() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("1", "updated"); + + assertEquals(1, map.size()); + assertEquals("updated", map.get("1")); + assertEquals(2, map.nextOpenIndex()); + } + + @Test + public void overwriteExistingNonNumericKeyDoesNotAffectTracking() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("foo", "foo"); + map.put("foo", "updated"); + + assertEquals(2, map.nextOpenIndex()); + } + + @Test + public void removeExistingNumericKeyReopensThatSlot() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("2", "two"); + map.put("3", "three"); + + map.remove("2"); + + assertEquals(2, map.nextOpenIndex()); + } + + @Test + public void removeFirstNumericKeyReopensOne() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("2", "two"); + + map.remove("1"); + + assertEquals(1, map.nextOpenIndex()); + } + + @Test + public void removeLastNumericKeyDoesNotAffectEarlierGapDetection() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("2", "two"); + map.put("4", "four"); + + map.remove("4"); + + assertEquals(3, map.nextOpenIndex()); + } + + @Test + public void removeNonNumericKeyDoesNotAffectTracking() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("foo", "foo"); + + map.remove("foo"); + + assertEquals(2, map.nextOpenIndex()); + } + + @Test + public void removeMissingKeyDoesNothing() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("2", "two"); + + map.remove("9"); + + assertEquals(3, map.nextOpenIndex()); + } + + @Test + public void addReusesFirstGap() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("3", "three"); + + map.add("two"); + + assertEquals("two", map.get("2")); + assertEquals(4, map.nextOpenIndex()); + } + + @Test + public void repeatedAddCreatesSequentialNumericKeys() { + IndexTrackingTreeMap map = newMap(); + + map.add("one"); + map.add("two"); + map.add("three"); + + assertEquals("one", map.get("1")); + assertEquals("two", map.get("2")); + assertEquals("three", map.get("3")); + assertEquals(4, map.nextOpenIndex()); + } + + @Test + public void zeroIsIgnoredAsNumericKey() { + IndexTrackingTreeMap map = newMap(); + map.put("0", "zero"); + + assertEquals(1, map.nextOpenIndex()); + } + + @Test + public void leadingZeroKeyBehaviorIsExplicit() { + IndexTrackingTreeMap map = newMap(); + map.put("01", "leading-zero"); + + assertEquals(1, map.nextOpenIndex()); + } + + @Test + public void alphaNumericKeyIsIgnored() { + IndexTrackingTreeMap map = newMap(); + map.put("1a", "value"); + map.put("a1", "value"); + + assertEquals(1, map.nextOpenIndex()); + } + + @Test + public void mapIndicesViewIsCorrect() { + IndexTrackingTreeMap map = new IndexTrackingTreeMap<>(); + map.put("1", "one"); + Map subMap = Map.of("a", "b"); + map.put("sub", subMap); + + Collection indices = map.mapIndices(); + assertEquals(1, indices.size()); + assertTrue(indices.contains("sub")); + + assertThrows(UnsupportedOperationException.class, () -> indices.add("other")); + } + + @Test + public void putNullValueRemovesMapping() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("1", null); + + assertNull(map.get("1")); + assertEquals(1, map.nextOpenIndex()); + } + + @Test + public void addMapValueUpdatesMapIndices() { + IndexTrackingTreeMap map = new IndexTrackingTreeMap<>(); + Map subMap = Map.of("a", "b"); + map.add(subMap); + + assertEquals(subMap, map.get("1")); + assertTrue(map.mapIndices().contains("1")); + } + + @Test + public void removeMapValueUpdatesMapIndices() { + IndexTrackingTreeMap map = new IndexTrackingTreeMap<>(); + Map subMap = Map.of("a", "b"); + map.put("sub", subMap); + assertTrue(map.mapIndices().contains("sub")); + + map.remove("sub"); + assertFalse(map.mapIndices().contains("sub")); + } + + @Test + public void clearResetsTracking() { + IndexTrackingTreeMap map = newMap(); + map.put("1", "one"); + map.put("2", "two"); + + map.clear(); + + //noinspection ConstantValue + assertTrue(map.isEmpty()); + assertEquals(1, map.nextOpenIndex()); + } + + @Test + public void putAllKeepsTrackingCorrect() { + IndexTrackingTreeMap map = newMap(); + + map.putAll(Map.of( + "1", "one", + "3", "three", + "foo", "foo" + )); + + assertEquals(2, map.nextOpenIndex()); + } + + @Test + public void largeNonParsableIntegerStringThrows() { + IndexTrackingTreeMap map = newMap(); + map.put("999999999999999999999999", "huge"); + assertEquals(1, map.size()); + assertEquals(1, map.nextOpenIndex()); + assertEquals("huge", map.get("999999999999999999999999")); + } + +}