package squidpony.squidmath; import squidpony.ArrayTools; import squidpony.annotation.Beta; import java.util.Collection; import java.util.Iterator; import java.util.SortedSet; /** * An ordered multi-directional "map" that keeps 1 or more keysets organized so you can look up one key given a key in * another keyset, and do the same for the indices of keys. Does not have generic type parameters, which is needed * because we handle arbitrary counts of keysets, and each keyset could have its own type. You can use most of the * normal Map methods here, though they often need an int as their first argument, {@code which}, that specifies which * keyset the method applies to. For example, {@link #contains(int, Object)} checks for the presence of the second * parameter in the keyset specified by the first parameter. Adding items to a MultiKey uses {@link #put(Object...)}, * and that does not take a {@code which} parameter because it needs to add an item to every keyset to succeed, and will * do nothing if the array or varargs passed to it has a different length than the {@link #keyCount} of the MultiKey. * Created by Tommy Ettinger on 10/23/2016. */ @Beta @SuppressWarnings("unchecked") public class MultiKey { public final int keyCount; Arrangement[] keys; /** * Constructs an empty MultiKey. */ public MultiKey() { this(3, 16, 0.5f); } /** * Constructs a MultiKey with the expected number of indices to hold (the number of items in each keyset is always * the same, and this will be more efficient if expected is greater than that number). * @param expected how many items this should be ready to hold; can resize if needed */ public MultiKey(int keyCount, int expected) { this(keyCount, expected, 0.5f); } /** * Constructs a MultiKey with the expected number of indices to hold (the number of items in each keyset is always * the same, and this will be more efficient if expected is greater than that number) and the load factor to use, * between 0.1f and 0.8f usually (using load factors higher than 0.8f can cause problems). * @param expected the amount of indices (the number of items in each keyset is always the same) this should hold * @param f the load factor, probably between 0.1f and 0.8f */ public MultiKey(int keyCount, int expected, float f) { this.keyCount = keyCount; keys = new Arrangement[keyCount]; for (int i = 0; i < keyCount; i++) { keys[i] = new Arrangement(expected, f); } } /** * Constructs a MultiKey from a Collection of Iterables that will be processed in tandem, adding a unique item from * each keyset in keysets if and only if it can also add a unique item from all other keysets, otherwise skipping * that iteration in each Iterable. * @param keysets a Collection of Iterable data structures, each containing items that should all be unique */ public MultiKey(Collection<Iterable> keysets) { if (keysets == null) { keyCount = 0; keys = new Arrangement[0]; } else { keyCount = keysets.size(); keys = new Arrangement[keyCount]; for (int i = 0; i < keyCount; i++) { keys[i] = new Arrangement(); } putAll(keysets); } } public MultiKey(MultiKey other) { if(other == null) { keyCount = 0; keys = new Arrangement[0]; } else { keyCount = other.keyCount; keys = new Arrangement[keyCount]; for (int i = 0; i < keyCount; i++) { keys[i] = new Arrangement(other.keys[i]); } } } public MultiKey(Arrangement[] keysets) { if(keysets == null) { keyCount = 0; keys = new Arrangement[0]; } else { keyCount = keysets.length; keys = new Arrangement[keyCount]; int minLength = Integer.MAX_VALUE; for (int k = 0; k < keyCount; k++) { if(keysets[k] == null) return; minLength = Math.min(minLength, keysets[k].size); } for (int k = 0; k < keyCount; k++) { keys[k] = keysets[k].take(minLength); } } } /** * Returns true if this contains key in the keyset specified by which. * @param which which keyset to check in * @param key the key to check the presence of * @return true if key is present in the keyset at which; false otherwise */ public boolean contains(int which, Object key) { if(which >= 0 && which < keyCount) return keys[which].containsKey(key); return false; } /** * Returns true if index is between 0 (inclusive) and {@link #size()} (exclusive), or false otherwise. * @param index the index to check * @return true if index is a valid index in the ordering of this MultiKey */ public boolean containsIndex(int index) { if(keyCount > 0) return keys[0].containsValue(index); return false; } /** * Given an index of a keyset (which) and an Object key, finds the position in the ordering that key has in the * keyset at which, or -1 if key is not present. * <br> * Unlike {@link java.util.List#indexOf(Object)}, this runs in constant time. * @param which which keyset to check in * @param key the key to find the position of * @return the int index of key in the ordering, or -1 if it is not present */ public int indexOf(int which, Object key) { if(which >= 0 && which < keyCount) return keys[which].getInt(key); return -1; } /** * Given an index of a keyset (which) and an int index, finds the associated key in the keyset specified by which * (using index as a point in the ordering). * @param which which keyset to get from * @param index an int index into this MultiKey * @return the key (in the keyset found by which) with index for its position in the ordering, or null if index or which was invalid */ public Object getAt(int which, int index) { if(which >= 0 && which < keyCount) return keys[which].keyAt(index); return null; } /** * Given an int index, finds the associated keys at all keysets (using index as a point in the ordering) and returns * them as a newly-allocated Object array. * @param index an int index into this MultiKey * @return the array of keys with index for their position in the ordering, or an array of null if index was invalid * @see #getAllAt(int, Object[]) getAllAt can avoid allocating a new array each time */ public Object[] getAllAt(int index) { Object[] os = new Object[keyCount]; for (int i = 0; i < keyCount; i++) { os[i] = keys[i].keyAt(index); } return os; } /** * Given an int index and an Object array to reuse, finds the associated keys at all keysets (using index as a point * in the ordering) and fills into with those keys, up to keyCount items. * @param index an int index into this MultiKey * @param into an Object array to reuse and fill with items; will be returned as-is if smaller than keyCount * @return the array of keys with index for their position in the ordering, or an array of null if index was invalid */ public Object[] getAllAt(int index, Object[] into) { if (into != null && into.length >= keyCount) { for (int i = 0; i < keyCount; i++) { into[i] = keys[i].keyAt(index); } } return into; } /** * Given an index of the keyset to look up a key in (lookingUp), an index of the keyset to get from (getting), and * an Object key to look up (key), finds the Object key in the keyset specified by getting that is associated with * key in the keyset specified by lookingUp. key and the returned value will be at the same point in the ordering. * @param lookingUp which keyset to look up the {@code key} parameter in * @param getting which keyset to get a value from * @param key an object to use as a key, which should be the right type for the keyset at lookingUp * @return the object from getting's keyset that is associated with key, or null if key was not present */ public Object getFrom(int lookingUp, int getting, Object key) { if(lookingUp >= 0 && lookingUp < keyCount && getting >= 0 && getting < keyCount) return keys[getting].keyAt(keys[lookingUp].getInt(key)); return null; } /** * Gets a random key from the keyset specified by which using the given RNG. * @param which which keyset to get a random key from * @param random generates a random index to get a key with * @return a randomly chosen key from the keyset specified, or null if this is empty */ public Object randomKey(int which, RNG random) { if(which >= 0 && which < keyCount) return keys[which].randomKey(random); return null; } /** * Gets a random key from a random keyset in this MultiKey using the given RNG. * @param random generates a random keyset index and random item index, to get a random key * @return a randomly chosen Object key from possibly any keyset in this, or null if this is empty */ public Object randomKey(RNG random) { if(keyCount > 0) return keys[random.nextIntHasty(keyCount)].randomKey(random); return null; } /** * In the keyset specified by {@code which}, changes an existing key, {@code past}, to another key, {@code future}, * if past exists in that keyset and future does not yet exist in that keyset. This will retain past's point in the * ordering for future, so the associated other key(s) will still be associated in the same way. * @param which which keyset to alter the items in * @param past a key, that must exist in the keyset specified by which, and will be changed * @param future a key, that cannot currently exist in the keyset specified by which, but will if this succeeds * @return this for chaining */ public MultiKey alter(int which, Object past, Object future) { if(which >= 0 && which < keyCount && keys[which].containsKey(past) && !keys[which].containsKey(future)) keys[which].alter(past, future); return this; } /** * In the keyset specified by {@code which}, changes the key at {@code index} to another key, {@code future}, if * index is valid and future does not yet exist in that keyset. The position in the ordering for future will be the * same as index, and the same as the key this replaced, if this succeeds, so the other key(s) at that position will * still be associated in the same way. * @param which which keyset to alter the items in * @param index a position in the ordering to change; must be at least 0 and less than {@link #size()} * @param future a key, that cannot currently exist in the keyset specified by which, but will if this succeeds * @return this for chaining */ public MultiKey alterAt(int which, int index, Object future) { if(which >= 0 && which < keyCount && !keys[which].containsKey(future) && index >= 0 && index < keys[which].size) keys[which].alter(keys[which].keyAt(index), future); return this; } /** * Adds a key to each keyset at the same point in the ordering (the end) of this MultiKey. The length of k must * match the keyCount of this MultiKey, and the nth item in k will go into the nth keyset. No item in k can be * present in the matching keyset in this before this is called. If you want to change or update an existing key, * use {@link #alter(int, Object, Object)} or {@link #alterAt(int, int, Object)}. * @param k an array or varargs of keys to add after the last index of this MultiKey; length must equal keyCount * @return true if this collection changed as a result of this call */ public boolean put(Object... k) { if(k != null && keyCount > 0 && k.length == keyCount) { for (int i = 0; i < keyCount; i++) { if(keys[i].containsKey(k[i])) return false; } for (int i = 0; i < keyCount; i++) { keys[i].add(k[i]); } return true; } return false; } /** * Goes through all Iterable items in {@code k} and adds their unique items into their corresponding keyset at the * end. If an item from the nth Iterable is already present in the nth keyset in this when this would add one, this * will not put any keys at that point in the iteration order, and will place the next unique group of items it * finds in the arguments at that position instead. * @param k a Collection of Iterable s of keys to add to their respective keysets; should all be unique (like a Set) * @return true if this collection changed as a result of this call */ public boolean putAll(Collection<Iterable> k) { if(k == null || k.size() != keyCount) return false; boolean changed = false; Iterator[] its = new Iterator[keyCount]; int idx = 0; for (Iterable it : k) { its[idx++] = it.iterator(); } Object[] os = new Object[keyCount]; while (true) { for (int i = 0; i < keyCount; i++) { if(!its[i].hasNext()) return changed; os[i] = its[i].next(); } changed = put(os) || changed; } } /** * Puts all unique keys in {@code other} into this MultiKey, respecting other's ordering. If a key in other is * already present when this would add one, this will not put the keys at that point in the iteration * order, and will place the next unique items it finds in other at that position instead. * @param other another MultiKey collection with the same keyCount * @return true if this collection changed as a result of this call */ public boolean putAll(MultiKey other) { if(other == null || other.keyCount != keyCount) return false; boolean changed = false; int sz = other.size(); Object[] os = new Object[keyCount]; for (int i = 0; i < sz; i++) { changed = put(other.getAllAt(i, os)) || changed; } return changed; } /** * Adds a key to each keyset at the given index in the ordering of this MultiKey. The length of k must * match the keyCount of this MultiKey, and the nth item in k will go into the nth keyset. No item in k can be * present in the matching keyset in this before this is called. If you want to change or update an existing key, * use {@link #alter(int, Object, Object)} or {@link #alterAt(int, int, Object)}. * @param k an array or varargs of keys to add after the last index of this MultiKey; length must equal keyCount * @return true if this collection changed as a result of this call */ public boolean putAt(int index, Object... k) { if(k != null && keyCount > 0 && k.length == keyCount) { for (int i = 0; i < keyCount; i++) { if(keys[i].containsKey(k[i])) return false; } for (int i = 0; i < keyCount; i++) { keys[i].addAt(index, k[i]); } return true; } return false; } /** * Removes a given Object key from the keyset specified by which, if {@code removing} exists in that keyset, and * also removes any keys associated with its point in the ordering. * @param which which keyset to remove the item from; if {@code removing} isn't in that keyset, this does nothing * @param removing the key to remove * @return this for chaining */ public MultiKey remove(int which, Object removing) { if(which >= 0 && which < keyCount) { int i = keys[which].removeInt(removing); if(i >= 0) { for (int j = 0; j < keyCount; j++) { if(j != which) keys[j].removeAt(i); } } } return this; } /** * Removes a given point in the ordering, if {@code index} is at least 0 and less than {@link #size()}. * @param index the position in the ordering to remove * @return this for chaining */ public MultiKey removeAt(int index) { if(index >= 0 && keyCount > 0 && index < keys[0].size) { for (int i = 0; i < keyCount; i++) { keys[i].removeAt(index); } } return this; } /** * Reorders this MultiKey using {@code ordering}, which have the same length as this MultiKey's {@link #size()} * and can be generated with {@link ArrayTools#range(int)} (which, if applied, would produce no * change to the current ordering), {@link RNG#randomOrdering(int)} (which gives a random ordering, and if * applied immediately would be the same as calling {@link #shuffle(RNG)}), or made in some other way. If you * already have an ordering and want to make a different ordering that can undo the change, you can use * {@link ArrayTools#invertOrdering(int[])} called on the original ordering. * @param ordering an int array or vararg that should contain each int from 0 to {@link #size()} (or less) * @return this for chaining */ public MultiKey reorder(int... ordering) { if(ordering != null) { for (int i = 0; i < keyCount; i++) { keys[i].reorder(ordering); } } return this; } /** * Generates a random ordering with rng and applies the same ordering to all kinds of keys this has; they will * maintain their current association to other keys but their ordering/indices will change. * @param rng an RNG to produce the random ordering this will use * @return this for chaining */ public MultiKey shuffle(RNG rng) { if(keyCount > 0) { int[] ordering = rng.randomOrdering(keys[0].size); for (int i = 0; i < keyCount; i++) { keys[i].reorder(ordering); } } return this; } /** * Creates a new iterator over the keys this holds in the keyset specified by which. This can be problematic for * garbage collection if called very frequently; it may be better to access items by index (which also lets you * access other keys associated with that index) using {@link #getAt(int, int)} in a for(int i=0...) loop. * @return a newly-created iterator over this MultiKey's keys in the specified keyset */ public Iterator iterator(int which) { if(which >= 0 && which < keyCount) return keys[which].iterator(); return null; } /** * Gets and caches the keys in the keyset specified by which as a Collection that implements SortedSet (and so also * implements Set). * @param which which keyset to get as a separate value * @return the keys from the keyset specified by which, as a SortedSet */ public SortedSet getSet(int which) { if(which >= 0 && which < keyCount) return keys[which].keySet(); return null; } /** * To be called sparingly, since this allocates a new OrderedSet instead of reusing one. * @param which which keyset to get as a separate value * @return the keys from the keyset specified by which, as an OrderedSet */ public OrderedSet getOrderedSet(int which) { if(which >= 0 && which < keyCount) return keys[which].keysAsOrderedSet(); return null; } public int keyCount() { return keyCount; } public int valueCount() { return 0; } public int size() { return (keyCount > 0) ? keys[0].size : 0; } public boolean isEmpty() { return keyCount > 0 && keys[0].size > 0; } }