groovy.lang.ObjectRange Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 groovy.lang;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.IteratorClosureAdapter;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.AbstractList;
import java.util.Iterator;
import java.util.List;
/**
* Represents an inclusive list of objects from a value to a value using
* comparators.
*
* Note: This class is similar to {@link IntRange}. If you make any changes to this
* class, you might consider making parallel changes to {@link IntRange}.
*/
public class ObjectRange extends AbstractList implements Range {
/**
* The first value in the range.
*/
private Comparable from;
/**
* The last value in the range.
*/
private Comparable to;
/**
* The cached size, or -1 if not yet computed
*/
private int size = -1;
/**
* true
if the range counts backwards from to
to from
.
*/
private final boolean reverse;
/**
* Creates a new {@link ObjectRange}. Creates a reversed range if
* from
< to
.
*
* @param from the first value in the range.
* @param to the last value in the range.
*/
public ObjectRange(Comparable from, Comparable to) {
if (from == null) {
throw new IllegalArgumentException("Must specify a non-null value for the 'from' index in a Range");
}
if (to == null) {
throw new IllegalArgumentException("Must specify a non-null value for the 'to' index in a Range");
}
try {
this.reverse = ScriptBytecodeAdapter.compareGreaterThan(from, to);
} catch (ClassCastException cce) {
throw new IllegalArgumentException("Unable to create range due to incompatible types: " + from.getClass().getSimpleName() + ".." + to.getClass().getSimpleName() + " (possible missing brackets around range?)", cce);
}
if (this.reverse) {
constructorHelper(to, from);
} else {
constructorHelper(from, to);
}
}
public ObjectRange(Comparable from, Comparable to, boolean reverse) {
constructorHelper(from, to);
this.reverse = reverse;
}
private void constructorHelper(Comparable from, Comparable to) {
if (from instanceof Short) {
from = ((Short) from).intValue();
} else if (from instanceof Float) {
from = ((Float) from).doubleValue();
}
if (to instanceof Short) {
to = ((Short) to).intValue();
} else if (to instanceof Float) {
to = ((Float) to).doubleValue();
}
if (from instanceof Integer && to instanceof Long) {
from = ((Integer) from).longValue();
} else if (to instanceof Integer && from instanceof Long) {
to = ((Integer) to).longValue();
}
// TODO: should we care about different types here?
if (from.getClass() == to.getClass()) {
this.from = from;
this.to = to;
} else {
this.from = normaliseStringType(from);
this.to = normaliseStringType(to);
}
if (from instanceof String || to instanceof String) {
// this test depends deeply on the String.next implementation
// 009.next is 00:, not 010
final String start = from.toString();
final String end = to.toString();
if (start.length() > end.length()) {
throw new IllegalArgumentException("Incompatible Strings for Range: starting String is longer than ending string");
}
final int length = Math.min(start.length(), end.length());
int i;
for (i = 0; i < length; i++) {
if (start.charAt(i) != end.charAt(i)) {
break;
}
}
if (i < length - 1) {
throw new IllegalArgumentException("Incompatible Strings for Range: String#next() will not reach the expected value");
}
}
}
/**
* {@inheritDoc}
*/
public boolean equals(Object that) {
return (that instanceof ObjectRange) ? equals((ObjectRange) that) : super.equals(that);
}
/**
* Compares an {@link ObjectRange} to another {@link ObjectRange}.
*
* @param that the object to check equality with
* @return true
if the ranges are equal
*/
public boolean equals(ObjectRange that) {
return that != null
&& reverse == that.reverse
&& DefaultTypeTransformation.compareEqual(from, that.from)
&& DefaultTypeTransformation.compareEqual(to, that.to);
}
/**
* {@inheritDoc}
*/
@Override
public Comparable getFrom() {
return from;
}
/**
* {@inheritDoc}
*/
@Override
public Comparable getTo() {
return to;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isReverse() {
return reverse;
}
/**
* {@inheritDoc}
*/
@Override
public Object get(int index) {
if (index < 0) {
throw new IndexOutOfBoundsException("Index: " + index + " should not be negative");
}
if (index >= size()) {
throw new IndexOutOfBoundsException("Index: " + index + " is too big for range: " + this);
}
Object value;
if (reverse) {
value = to;
for (int i = 0; i < index; i++) {
value = decrement(value);
}
} else {
value = from;
for (int i = 0; i < index; i++) {
value = increment(value);
}
}
return value;
}
/**
* {@inheritDoc}
*/
public Iterator iterator() {
return new Iterator() {
private int index;
private Object value = reverse ? to : from;
public boolean hasNext() {
return index < size();
}
public Object next() {
if (index++ > 0) {
if (index > size()) {
value = null;
} else {
if (reverse) {
value = decrement(value);
} else {
value = increment(value);
}
}
}
return value;
}
public void remove() {
ObjectRange.this.remove(index);
}
};
}
/**
* Checks whether a value is between the from and to values of a Range
*
* @param value the value of interest
* @return true if the value is within the bounds
*/
@Override
public boolean containsWithinBounds(Object value) {
if (value instanceof Comparable) {
final int result = compareTo(from, (Comparable) value);
return result == 0 || result < 0 && compareTo(to, (Comparable) value) >= 0;
}
return contains(value);
}
private int compareTo(Comparable first, Comparable second) {
return DefaultGroovyMethods.numberAwareCompareTo(first, second);
}
/**
* {@inheritDoc}
*/
@Override
public int size() {
if (size == -1) {
if ((from instanceof Integer || from instanceof Long)
&& (to instanceof Integer || to instanceof Long)) {
// let's fast calculate the size
final long fromNum = ((Number) from).longValue();
final long toNum = ((Number) to).longValue();
size = (int) (toNum - fromNum + 1);
} else if (from instanceof Character && to instanceof Character) {
// let's fast calculate the size
final char fromNum = (Character) from;
final char toNum = (Character) to;
size = toNum - fromNum + 1;
} else if (from instanceof BigDecimal || to instanceof BigDecimal ||
from instanceof BigInteger || to instanceof BigInteger) {
// let's fast calculate the size
final BigDecimal fromNum = new BigDecimal(from.toString());
final BigDecimal toNum = new BigDecimal(to.toString());
final BigInteger sizeNum = toNum.subtract(fromNum).add(new BigDecimal(1.0)).toBigInteger();
size = sizeNum.intValue();
} else {
// let's lazily calculate the size
size = 0;
Comparable first = from;
Comparable value = from;
while (compareTo(to, value) >= 0) {
value = (Comparable) increment(value);
size++;
if (compareTo(first, value) >= 0) break; // handle back to beginning due to modulo incrementing
}
}
}
return size;
}
/**
* {@inheritDoc}
*/
@Override
public List subList(int fromIndex, int toIndex) {
if (fromIndex < 0) {
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
}
if (toIndex > size()) {
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
}
if (fromIndex > toIndex) {
throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")");
}
if (fromIndex == toIndex) {
return new EmptyRange(from);
}
return new ObjectRange((Comparable) get(fromIndex), (Comparable) get(--toIndex), reverse);
}
/**
* {@inheritDoc}
*/
public String toString() {
return reverse ? "" + to + ".." + from : "" + from + ".." + to;
}
/**
* {@inheritDoc}
*/
@Override
public String inspect() {
final String toText = InvokerHelper.inspect(to);
final String fromText = InvokerHelper.inspect(from);
return reverse ? "" + toText + ".." + fromText : "" + fromText + ".." + toText;
}
/**
* iterates over all values and returns true if one value matches.
* Also see containsWithinBounds.
*/
@Override
public boolean contains(Object value) {
final Iterator iter = iterator();
if (value == null) {
return false;
}
while (iter.hasNext()) {
try {
if (DefaultTypeTransformation.compareEqual(value, iter.next())) return true;
} catch (ClassCastException e) {
return false;
}
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public void step(int step, Closure closure) {
if (step == 0) {
if (compareTo(from, to) != 0) {
throw new GroovyRuntimeException("Infinite loop detected due to step size of 0");
} else {
return; // from == to and step == 0, nothing to do, so return
}
}
if (reverse) {
step = -step;
}
if (step > 0) {
final Comparable first = from;
Comparable value = from;
while (compareTo(value, to) <= 0) {
closure.call(value);
for (int i = 0; i < step; i++) {
value = (Comparable) increment(value);
if (compareTo(value, first) <= 0) return;
}
}
} else {
step = -step;
final Comparable first = to;
Comparable value = to;
while (compareTo(value, from) >= 0) {
closure.call(value);
for (int i = 0; i < step; i++) {
value = (Comparable) decrement(value);
if (compareTo(value, first) >= 0) return;
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public List step(int step) {
final IteratorClosureAdapter adapter = new IteratorClosureAdapter(this);
step(step, adapter);
return adapter.asList();
}
/**
* Increments by one
*
* @param value the value to increment
* @return the incremented value
*/
protected Object increment(Object value) {
return InvokerHelper.invokeMethod(value, "next", null);
}
/**
* Decrements by one
*
* @param value the value to decrement
* @return the decremented value
*/
protected Object decrement(Object value) {
return InvokerHelper.invokeMethod(value, "previous", null);
}
private static Comparable normaliseStringType(final Comparable operand) {
if (operand instanceof Character) {
return (int) (Character) operand;
}
if (operand instanceof String) {
final String string = (String) operand;
if (string.length() == 1) {
return (int) string.charAt(0);
}
return string;
}
return operand;
}
}