/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.foundationdb.util;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
public class SparseArray<T> {
/**
* Returns the greatest index for which {@link #isDefined(int)} would return <tt>true</tt>
* @return the logical "size" of this sparse array
*/
public int lastDefinedIndex() {
return definedElements.length();
}
/**
* Returns whether this array could be expressed as a compact array. That is, whether all indexes
* less than {@link #lastDefinedIndex()} would return <tt>true</tt> for {@link #isDefined(int)}. If this method
* returns <tt>true</tt>, it is safe to call {@link #toList()}
* @return
*/
public boolean isCompactable() {
return definedElements.length() == definedElements.cardinality();
}
@SuppressWarnings("unchecked") // T[] is just erased to Object[] anyway, which is what we have
public List<T> toList() {
if (!isCompactable())
throw new IllegalArgumentException("Not compactable");
T[] arrayCopy = Arrays.copyOf((T[])internalArray, definedElements.length());
return Arrays.asList(arrayCopy);
}
/**
* Gets the element at the specified index. If that element had not been previously defined, its initial
* value will be taken from {@link #initialValue()}. When this method returns, the element at the index
* will always be defined.
* @param index the index to retrieve
* @return the element at the specified index
*/
public T get(int index) {
if (index < 0)
throw new IndexOutOfBoundsException(Integer.toString(index));
if (!definedElements.get(index)) {
ensureCapacity(index);
internalArray[index] = initialValue();
definedElements.set(index);
}
return internalGet(index);
}
public T getIfDefined(int index) {
if (index < 0)
throw new IndexOutOfBoundsException(Integer.toString(index));
if (!definedElements.get(index))
throw new IllegalArgumentException("undefined value at index " + index);
return internalGet(index);
}
/**
* Sets the element at the specified index. This also marks that index as defined. This method returns the old
* value at the index, or {@code null} if it was undefined. This means that if defined methods can ever be null
* in your usage (either because of sets or initial values), you cannot use this method to determine whether the
* old value had been defined. Use {@link #isDefined(int)} instead.
* @param index the index to set
* @param item the new value
* @return the old element at this index, or {@code null} if it was undefined
*/
public T set(int index, T item) {
if (index < 0)
throw new IndexOutOfBoundsException(Integer.toString(index));
ensureCapacity(index);
T old = internalGet(index);
// if old != null, this element was definitely defined before, so don't bother redefining it.
// if old == null, this element may or may not have been defined, but redefining it is idempotent
if (old == null)
definedElements.set(index);
internalArray[index] = item;
return old;
}
public boolean isDefined(int index) {
return definedElements.get(index);
}
public boolean isEmpty() {
return definedElements.isEmpty();
}
public void clear() {
Arrays.fill(internalArray, null);
definedElements.clear();
}
public String describeElements() {
StringBuilder sb = new StringBuilder();
describeElements(sb);
return sb.toString();
}
public StringBuilder describeElements(StringBuilder sb) {
sb.append('[');
int ilen = sb.length();
int size = definedElements.size();
for (int i = 0; i < size; ++i) {
if (isDefined(i))
sb.append(internalGet(i)).append(", ");
}
if (sb.length() > ilen) // sb is not just the initial '['
sb.setLength(sb.length() - 2); // snip off the trailing ", "
sb.append(']');
return sb;
}
protected T initialValue() {
return null;
}
@Override
public String toString() {
return "SparseArray(" + definedElements.cardinality() + " defined: " + definedElements + ')';
}
// intended for testing
int currentCapacity() {
return internalArray.length;
}
private void ensureCapacity(int index) {
if (internalArray.length <= index) {
int newSize = internalArray.length * GROW_FACTOR;
if (newSize <= index)
newSize = index + 1;
Object[] newInternalArray = new Object[newSize];
System.arraycopy(internalArray, 0, newInternalArray, 0, internalArray.length);
internalArray = newInternalArray;
}
}
@SuppressWarnings("unchecked")
private T internalGet(int index) {
return (T) internalArray[index];
}
public SparseArray(int initialCapacity) {
internalArray = new Object[initialCapacity];
definedElements = new BitSet(initialCapacity);
}
public SparseArray() {
this(INITIAL_SIZE);
}
private Object[] internalArray;
private BitSet definedElements;
private static final int INITIAL_SIZE = 10;
private static final int GROW_FACTOR = 2;
}