/*
* Copyright 2013 Cameron Beccario
*
* 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 net.nullschool.collect.basic;
import net.nullschool.collect.IteratorTools;
import net.nullschool.collect.MapIterator;
import net.nullschool.util.ObjectTools;
import java.util.*;
import static java.lang.Math.min;
import static java.util.Objects.requireNonNull;
import static net.nullschool.util.ArrayTools.*;
/**
* 2013-03-15<p/>
*
* @author Cameron Beccario
*/
class BasicTools {
private BasicTools() {
throw new AssertionError();
}
/**
* A utility class to hold the keys and values of a columnized map. Map entries are represented as
* {@code {keys[i], values[i]}} for every index i.
*/
static class MapColumns {
final Object[] keys;
final Object[] values;
MapColumns(Object[] keys, Object[] values) {
this.keys = keys;
this.values = values;
}
}
private static final MapColumns EMPTY_MAP_COLUMNS = new MapColumns(EMPTY_OBJECT_ARRAY, EMPTY_OBJECT_ARRAY);
/**
* Copies the specified array into a new array having component type Object.
*
* @param original the array to copy.
* @return all the elements of the original array in a new Object[] instance.
* @throws NullPointerException if original is null.
*/
static Object[] copy(Object[] original) {
return Arrays.copyOf(original, original.length, Object[].class);
}
/**
* Copies the specified array into a new array of the specified length and having component type Object.
* The resulting array is either truncated or padded with nulls if newLength is less than or greater than
* the original array's length.
*
* @param original the array to copy.
* @return all the elements of the original array in a new Object[] instance.
* @throws NullPointerException if original is null.
* @throws NegativeArraySizeException if newLength is negative.
*/
static Object[] copy(Object[] original, int newLength) {
return Arrays.copyOf(original, newLength, Object[].class);
}
/**
* Copies the specified collection into a new array having component type Object.
*
* @param original the collection to copy.
* @return all the elements of the original collection in a new Object[] instance.
* @throws NullPointerException if the collection is null.
*/
static Object[] copy(Collection<?> original) {
// We must rely on the collection to allocate a correctly sized array for us because its size may
// be changing concurrently. Note the contract for toArray() lets us avoid a wasteful defensive
// copy of the result.
// noinspection ToArrayCallWithZeroLengthArrayArgument
return original.toArray(EMPTY_OBJECT_ARRAY);
}
/**
* Copies the elements of the specified iterator into a new array having component type Object.
*
* @param original the iterator to iterate.
* @return all the elements of the specified iterator in a new Object[] instance.
* @throws NullPointerException if the iterator is null.
*/
static Object[] copy(Iterator<?> original) {
List<Object> elements = new ArrayList<>();
while (original.hasNext()) {
elements.add(original.next());
}
return elements.toArray();
}
/**
* Copies the specified map into an array of keys and an array of values, represented as a MapColumns
* object. The ith index of the arrays represents entry {@code {ki, vi}} from the original map, and the
* array component types are both Object.
*
* @param original the map to copy.
* @return all the entries of the original array as a MapColumns object.
* @throws NullPointerException if original is null.
*/
static MapColumns copy(Map<?, ?> original) {
final int expectedSize = original.size();
if (expectedSize == 0) {
return EMPTY_MAP_COLUMNS;
}
Object[] keys = new Object[expectedSize];
Object[] values = new Object[expectedSize];
MapIterator<?, ?> iter = IteratorTools.newMapIterator(original);
int i;
for (i = 0; i < expectedSize && iter.hasNext(); i++) {
keys[i] = iter.next();
values[i] = iter.value();
}
if (!iter.hasNext()) {
// Check if the iterator had fewer iterations than expected. If so, truncate the resulting arrays.
return new MapColumns(
i < expectedSize ? copy(keys, i) : keys,
i < expectedSize ? copy(values, i) : values);
}
// The iterator has more iterations than expected. Just dump everything into lists and call toArray.
List<Object> allKeys = new ArrayList<>(expectedSize + 8);
List<Object> allValues = new ArrayList<>(expectedSize + 8);
Collections.addAll(allKeys, keys);
Collections.addAll(allValues, values);
do {
allKeys.add(iter.next());
allValues.add(iter.value());
} while (iter.hasNext());
return new MapColumns(allKeys.toArray(), allValues.toArray());
}
/**
* Returns a new array, having component type Object, that is the result of inserting an element into
* an existing array at the specified index. All elements occurring after {@code index} are shifted down
* to make room for the new element.
*
* @param original the array.
* @param index the index where the new element should be inserted.
* @param e the new element.
* @return a new Object[] instance having all the elements of the original array and with the new element
* inserted at the specified index.
* @throws NullPointerException if original is null.
* @throws IndexOutOfBoundsException if index is less than zero or greater than the length of the original.
*/
static Object[] insert(Object[] original, int index, Object e) {
Object[] result = new Object[original.length + 1];
System.arraycopy(original, 0, result, 0, index);
result[index] = e;
System.arraycopy(original, index, result, index + 1, original.length - index);
return result;
}
/**
* Returns a new array, having component type Object, that is the result of inserting all the elements
* of the specified collection into an existing array starting at the specified index. All original
* elements occurring after {@code index} are shifted down to make room for the new elements.
*
* @param original the array.
* @param index the starting index where the new elements should be inserted.
* @param c the collection of elements to insert.
* @return a new Object[] instance having all the elements of the original array and with the new elements
* inserted at the specified index.
* @throws NullPointerException if original or c is null.
* @throws IndexOutOfBoundsException if index is less than zero or greater than the length of the original.
*/
static Object[] insertAll(Object[] original, int index, Collection<?> c) {
final Object[] elements = c.toArray();
Object[] result = new Object[original.length + elements.length];
System.arraycopy(original, 0, result, 0, index);
System.arraycopy(elements, 0, result, index, elements.length);
System.arraycopy(original, index, result, index + elements.length, original.length - index);
return result;
}
/**
* Returns a new array, having component type Object, that is the result of replacing the element at
* the {@code index} with the specified element.
*
* @param original the array.
* @param index the index of the element to replace.
* @param e the new element to store in the array at the specified index.
* @return a new Object[] containing all elements of the original, but with the specified element replaced.
* @throws NullPointerException if original is null.
* @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the length of the original.
*/
static Object[] replace(Object[] original, int index, Object e) {
Object[] result = copy(original);
result[index] = e;
return result;
}
/**
* Returns a new array, having component type Object, that is the result of removing the element at
* the specified index. All elements occurring after {@code index} are shifted up to fill the gap left
* by thd deleted element.
*
* @param original the array.
* @param index the index of the element to delete.
* @return a new Object[] containing all elements of the original, but with the specified element removed.
* @throws NullPointerException if original is null.
* @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the length of the original.
* @throws NegativeArraySizeException if original is zero length.
*/
static Object[] delete(Object[] original, int index) {
final int resultLength = original.length - 1;
Object[] result = new Object[resultLength];
System.arraycopy(original, 0, result, 0, index);
System.arraycopy(original, index + 1, result, index, resultLength - index);
return result;
}
/**
* Returns a new array, having component type Object, that is the result of removing from the original array
* all elements contained in the specified collection. The collection's {@link Collection#contains contains}
* method is used to determine if the collection contains elements from {@code original}.
*
* @param original the array.
* @param c the collection of elements to delete.
* @return a new Object[] containing all elements of the original except those in c.
* @throws NullPointerException if original or c is null.
* @throws ClassCastException if any element of {@code original} is incompatible with the collection.
*/
static Object[] deleteAll(Object[] original, Collection<?> c) {
Object[] result = new Object[original.length];
requireNonNull(c);
int cursor = 0;
for (Object o : original) {
if (!c.contains(o)) {
result[cursor++] = o;
}
}
return cursor == result.length ? result : copy(result, cursor);
}
/**
* Returns new map columns, having component type Object, that is the result of removing from the original
* columns all keys contained in the specified collection. The collection's {@link Collection#contains contains}
* method is used to determine if the collection contains the elements in {@code keys}.
*
* @param keys the key column of the map.
* @param values the value column of the map.
* @param c the collection of keys to delete.
* @return a new MapColumns object containing all entries of the original columns except those in c.
* @throws NullPointerException if keys, values, or c is null.
* @throws ClassCastException if any element of {@code keys} is incompatible with the collection.
* @throws IndexOutOfBoundsException if {@code values} is smaller than {@code keys}.
*/
static MapColumns deleteAll(Object[] keys, Object[] values, Collection<?> c) {
final int length = min(keys.length, values.length);
Object[] resultKeys = new Object[length];
Object[] resultValues = new Object[length];
requireNonNull(c);
int cursor = 0;
for (int i = 0; i < length; i++) {
Object key = keys[i];
if (!c.contains(key)) {
resultKeys[cursor] = key;
resultValues[cursor++] = values[i];
}
}
return new MapColumns(
cursor < length ? copy(resultKeys, cursor) : resultKeys,
cursor < length ? copy(resultValues, cursor) : resultValues);
}
/**
* Returns a new array, having component type Object, that is the result of merging the original array's elements
* with all unique elements from the {@code additional} array not already contained in the {@code original} array.
* Elements are compared using {@link Object#equals}. Merged elements are appended to the end of the resulting
* array in the order they are encountered. Note that duplicate elements from the original array, if any, are
* <i>retained</i> in the result but duplicate elements from the additional array are <i>excluded</i> from the
* result. For example: {@code unionInto([1, 1], [1, 0, 0])} returns {@code [1, 1, 0]}.
*
* @param original the existing elements.
* @param additional the elements to union with the existing elements.
* @return a new Object[] instance containing all elements of {@code original} appended with unique elements
* from {@code additional}.
* @throws NullPointerException if either {@code original} or {@code additional} arrays are null.
*/
static Object[] unionInto(Object[] original, Object[] additional) {
// CONSIDER: this method is O(N^2). Switch to a different algorithm above a certain size?
Object[] result = copy(original, original.length + additional.length);
int cursor = original.length;
for (Object element : additional) {
// Do a linear search of the resulting elements found so far.
if (indexOf(element, result, 0, cursor) < 0) {
result[cursor++] = element;
}
}
// Truncate if array was not completely filled.
return cursor == result.length ? result : copy(result, cursor);
}
/**
* Returns a new array, having component type Object, that is the result of merging the original array's elements
* with all unique elements from the {@code additional} array not already contained in the {@code original} array.
* Elements are compared using {@link Comparator#compare}, or {@link Comparable natural ordering} if the comparator
* is null. The resulting array is sorted according to this ordering. Note that duplicate elements from the
* original array, if any, are <i>retained</i> in the result but duplicate elements from the additional array
* are <i>excluded</i> from the result. For example: {@code unionInto([1, 1], [1, 0, 0])} returns
* {@code [1, 1, 0]}.<p/>
*
* <em>NOTE:</em> If the ordering of the {@code original} array's elements is not consistent with the specified
* comparator (or natural ordering if the comparator is null), then the behavior of this method is undefined.
*
* @param original the existing elements, sorted with the ordering specified by comparator.
* @param additional the elements to union with the existing elements.
* @param comparator the comparison operator, or null if natural ordering is to be used.
* @return a new Object[] instance containing all elements of {@code original} appended with unique elements
* from {@code additional}.
* @throws NullPointerException if either {@code original} or {@code additional} arrays are null, or any
* two elements are null and the comparator is either null or does not permit
* null arguments.
* @throws ClassCastException if any two elements' types are not compatible for comparison.
*/
static Object[] unionInto(Object[] original, Object[] additional, Comparator<?> comparator) {
// CONSIDER: this method is O(N^2). Switch to a different algorithm above a certain size?
Object[] result = copy(original, original.length + additional.length);
// Cast to comparator of object is safe here because each comparator will do its own type checking when
// the compare method is invoked.
@SuppressWarnings("unchecked") Comparator<Object> c = (Comparator<Object>)comparator;
int cursor = original.length;
for (Object element : additional) {
// Do a binary search of the original sorted elements.
if (Arrays.binarySearch(result, 0, original.length, element, c) < 0) {
// Do a linear search of new unique elements found so far.
if (indexOf(element, result, original.length, cursor, c) < 0) {
if (cursor == 0) {
checkType(c, element); // This is the first item, so check comparability.
}
result[cursor++] = element;
}
}
}
// Truncate if array was not completely filled. Sort the result.
return sort(cursor == result.length ? result : copy(result, cursor), c);
}
/**
* Returns new map columns, having component type Object, that are the result of merging the original columns'
* entries with all unique entries from the additional columns not already contained in the original columns.
* Keys are compared using {@link Object#equals}. Merged entries are appended to the end of the resulting
* columns in the order they are encountered. Note that duplicate keys from the original columns, if any, are
* <i>retained</i> in the result but duplicate keys from the additional columns are <i>excluded</i> from the
* result. However, those duplicate keys' values are retained in the result just as would occur when calling
* {@link Map#put} on an existing key.
*
* @param originalKeys the existing keys.
* @param originalValues the corresponding existing values.
* @param additionalKeys the keys to union with the existing entries.
* @param additionalValues the values to union with the existing entries.
* @return a new MapColumns instance containing all original entries appended with the unique additional entries.
* @throws NullPointerException if any argument is null.
*/
static MapColumns unionInto(
Object[] originalKeys,
Object[] originalValues,
Object[] additionalKeys,
Object[] additionalValues) {
final int originalLength = min(originalKeys.length, originalValues.length);
final int additionalLength = min(additionalKeys.length, additionalValues.length);
final int length = originalLength + additionalLength;
Object[] resultKeys = copy(originalKeys, length);
Object[] resultValues = copy(originalValues, length);
int cursor = originalLength;
for (int i = 0; i < additionalLength; i++) {
Object newKey = additionalKeys[i];
Object newValue = additionalValues[i];
int index = indexOf(newKey, resultKeys, 0, cursor);
if (index < 0) {
// Add a new unique entry
resultKeys[cursor] = newKey;
resultValues[cursor++] = newValue;
}
else {
// Replace value of existing entry
resultValues[index] = newValue;
}
}
return new MapColumns(
cursor < length ? copy(resultKeys, cursor) : resultKeys,
cursor < length ? copy(resultValues, cursor) : resultValues);
}
/**
* Returns new map columns, having component type Object, that are the result of merging the original columns'
* entries with all unique entries from the additional columns not already contained in the original columns.
* Keys are compared using {@link Comparator#compare}, or {@link Comparable natural ordering} if the comparator
* is null. The resulting columns are sorted according to this ordering. Note that duplicate keys from the
* original columns, if any, are <i>retained</i> in the result but duplicate keys from the additional columns
* are <i>excluded</i> from the result. However, those duplicate keys' values are retained in the result just as
* would occur when calling {@link Map#put} on an existing key.<p/>
*
* <em>NOTE:</em> If the ordering of the original column's keys is not consistent with the specified
* comparator (or natural ordering if the comparator is null), then the behavior of this method is undefined.
*
* @param originalKeys the existing keys.
* @param originalValues the corresponding existing values.
* @param additionalKeys the keys to union with the existing entries.
* @param additionalValues the values to union with the existing entries.
* @return a new MapColumns instance containing all original entries appended with the unique additional entries.
* @throws NullPointerException if any argument is null, or any two keys are null and the comparator is either
* null or does not permit null arguments.
* @throws ClassCastException if any two keys' types are not compatible for comparison.
*/
static MapColumns unionInto(
Object[] originalKeys,
Object[] originalValues,
Object[] additionalKeys,
Object[] additionalValues,
Comparator<?> comparator) {
final int originalLength = min(originalKeys.length, originalValues.length);
final int additionalLength = min(additionalKeys.length, additionalValues.length);
final int length = originalLength + additionalLength;
Object[] resultKeys = copy(originalKeys, length);
Object[] resultValues = copy(originalValues, length);
// Cast to comparator of object is safe here because each comparator will do its own type checking when
// the compare method is invoked.
@SuppressWarnings("unchecked") Comparator<Object> c = (Comparator<Object>)comparator;
int cursor = originalLength;
for (int i = 0; i < additionalLength; i++) {
Object key = additionalKeys[i];
Object value = additionalValues[i];
int index = Arrays.binarySearch(resultKeys, 0, cursor, key, c);
if (index < 0) {
if (cursor == 0) {
checkType(c, key); // This is the first key, so check comparability.
}
// Need to insert, so shift everything down.
index = flip(index);
System.arraycopy(resultKeys, index, resultKeys, index + 1, cursor - index);
System.arraycopy(resultValues, index, resultValues, index + 1, cursor - index);
resultKeys[index] = key;
resultValues[index] = value;
cursor++;
}
else {
// Replace value of existing entry.
resultValues[index] = value;
}
}
return new MapColumns(
cursor < length ? copy(resultKeys, cursor) : resultKeys,
cursor < length ? copy(resultValues, cursor) : resultValues);
}
/**
* Flips a negative index into the corresponding proper positive index, as according to the formula described
* by {@link Arrays#binarySearch(Object[], Object)}: {@code i := (-insertion_point - 1)}. This method returns
* {@code insertion_point := (-1 - i)}.
*
* @param i the value to flip.
* @return the flipped value.
*/
static int flip(int i) {
return -1 - i;
}
/**
* Ensures the specified object is of a type compatible with the provided comparator.
*
* @param comparator the comparator.
* @param item the item to check.
* @return the item.
* @throws NullPointerException if item is null and the comparator is null or does not support null values.
* @throws ClassCastException if the item is of a type not compatible for comparison.
*/
static <T> T checkType(Comparator<? super T> comparator, T item) {
ObjectTools.compare(item, item, comparator);
return item;
}
}