/* * Copyright 2023 dorkbox, llc * * 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. */ /******************************************************************************* * Copyright 2011 LibGDX. * Mario Zechner @gmail.com> * Nathan Sweet @gmail.com> * * 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 dorkbox.collections import dorkbox.collections.Collections.allocateIterators import java.util.Comparator /** * A [ObjectSet] that also stores keys in an [ArrayList] using the insertion order. Null keys are not allowed. * * * [Iteration][.iterator] is ordered and faster than an unordered set. * * This class performs fast contains (typically O(1), worst case O(n) but that is rare in practice). Remove is somewhat slower due * to [.orderedItems]. Add may be slightly slower, depending on hash collisions. Hashcodes are rehashed to reduce * collisions and the need to resize. Load factors greater than 0.91 greatly increase the chances to resize to the next higher POT * size. * * * Unordered sets and maps are not designed to provide especially fast iteration. Iteration is faster with OrderedSet and * OrderedMap. * * * This implementation uses linear probing with the backward shift algorithm for removal. * * Hashcodes are rehashed using Fibonacci hashing, instead of the more common power-of-two mask, to better distribute poor * hashCodes (see [Malte Skarupke's blog post](https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/)). * * Linear probing continues to work even when all hashCodes collide, just more slowly. * * @author Nathan Sweet * @author Tommy Ettinger */ class OrderedSet : ObjectSet where T : Any, T : Comparable { companion object { val version = Collections.version fun with(vararg array: T): OrderedSet where T : Any, T : Comparable { val set = OrderedSet() set.addAll(*array) return set } } private val items: ArrayList @Transient var iterator1: OrderedSetIterator? = null @Transient var iterator2: OrderedSetIterator? = null constructor() { items = ArrayList() } constructor(initialCapacity: Int, loadFactor: Float) : super(initialCapacity, loadFactor) { items = ArrayList(initialCapacity) } constructor(initialCapacity: Int) : super(initialCapacity) { items = ArrayList(initialCapacity) } constructor(set: OrderedSet) : super(set) { items = ArrayList(set.items) } /** Sorts this array. The array elements must implement [Comparable]. This method is not thread safe. */ fun sort() { items.sort() } /** Sorts the array. This method is not thread safe. */ fun sort(comparator: Comparator) { items.sortWith(comparator) } override fun add(element: T): Boolean { if (!super.add(element)) return false items.add(element) return true } /** * Sets the key at the specfied index. Returns true if the key was added to the set or false if it was already in the set. If * this set already contains the key, the existing key's index is changed if needed and false is returned. */ fun add(key: T, index: Int): Boolean { if (!super.add(key)) { var oldIndex = -1 items.forEachIndexed { i, item -> if (item === key) { oldIndex = i return@forEachIndexed } } if (oldIndex != index) { val oldItem = items.removeAt(oldIndex) items.add(index, oldItem) } return false } items.add(index, key) return true } fun addAll(set: OrderedSet) { ensureCapacity(set.size) val keys = set.items var i = 0 val n = set.items.size while (i < n) { add(keys[i]) i++ } } override fun remove(element: T): Boolean { if (!super.remove(element)) return false items.remove(element) return true } fun removeIndex(index: Int): T { val itemAtIndex = items.removeAt(index) super.remove(itemAtIndex) return itemAtIndex } /** * Changes the item `before` to `after` without changing its position in the order. Returns true if `after` * has been added to the OrderedSet and `before` has been removed; returns false if `after` is already present or * `before` is not present. If you are iterating over an OrderedSet and have an index, you should prefer * [.alterIndex], which doesn't need to search for an index like this does and so can be faster. * * @param before an item that must be present for this to succeed * @param after an item that must not be in this set for this to succeed * * @return true if `before` was removed and `after` was added, false otherwise */ fun alter(before: T, after: T): Boolean { if (contains(after)) return false if (!super.remove(before)) return false super.add(after) items[items.indexOf(before)] = after return true } /** * Changes the item at the given `index` in the order to `after`, without changing the ordering of other items. If * `after` is already present, this returns false; it will also return false if `index` is invalid for the size of * this set. Otherwise, it returns true. Unlike [.alter], this operates in constant time. * * @param index the index in the order of the item to change; must be non-negative and less than [.size] * @param after the item that will replace the contents at `index`; this item must not be present for this to succeed * * @return true if `after` successfully replaced the contents at `index`, false otherwise */ fun alterIndex(index: Int, after: T): Boolean { if (index < 0 || index >= size || contains(after)) return false super.remove(items[index]) super.add(after) items[index] = after return true } override fun clear(maximumCapacity: Int) { items.clear() super.clear(maximumCapacity) } override fun clear() { items.clear() super.clear() } fun orderedItems(): ArrayList { return items } override fun iterator(): OrderedSetIterator { if (allocateIterators) return OrderedSetIterator(this) if (iterator1 == null) { iterator1 = OrderedSetIterator(this) iterator2 = OrderedSetIterator(this) } if (!iterator1!!.valid) { iterator1!!.reset() iterator1!!.valid = true iterator2!!.valid = false return iterator1 as OrderedSetIterator } iterator2!!.reset() iterator2!!.valid = true iterator1!!.valid = false return iterator2 as OrderedSetIterator } override fun toString(): String { if (size == 0) return "{}" val items = items val buffer = StringBuilder(32) buffer.append('{') buffer.append(items[0]) for (i in 1 until size) { buffer.append(", ") buffer.append(items[i]) } buffer.append('}') return buffer.toString() } override fun toString(separator: String): String { return items.joinToString(separator) } class OrderedSetIterator(set: OrderedSet) : ObjectSetIterator(set) where T : Any, T : Comparable { private val items: ArrayList init { items = set.items } override fun reset() { nextIndex = 0 hasNext = set.size > 0 } override fun next(): T { if (!hasNext) throw NoSuchElementException() if (!valid) throw RuntimeException("#iterator() cannot be used nested.") val key = items[nextIndex] nextIndex++ hasNext = nextIndex < set.size return key } override fun remove() { check(nextIndex >= 0) { "next must be called before remove." } nextIndex-- (set as OrderedSet<*>).removeIndex(nextIndex) } @Suppress("USELESS_CAST", "UNCHECKED_CAST") override fun toArray(): Array { return Array(set.size - nextIndex) { next() as Any } as Array } override fun toArray(array: Array): Array { var i = nextIndex while(hasNext) { array[i++] = next() } nextIndex = items.size hasNext = false return array } } }