/* * 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; import java.util.NoSuchElementException; /** * Represents an inclusive list of objects from a value to a value using * comparators. * <p> * 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<Comparable> implements Range<Comparable> { /** * The first value in the range. */ private final Comparable from; /** * The last value in the range. */ private final Comparable to; /** * The cached size, or -1 if not yet computed */ private int size = -1; /** * <code>true</code> if the range counts backwards from <code>to</code> to <code>from</code>. */ private final boolean reverse; /** * Creates a new {@link ObjectRange}. Creates a reversed range if * <code>from</code> < <code>to</code>. * * @param from the first value in the range. * @param to the last value in the range. */ public ObjectRange(Comparable from, Comparable to) { this(from, to, null); } /** * Creates a new {@link ObjectRange} assumes smaller <= larger, else behavior is undefined. * Caution: Prefer the other constructor when in doubt. * <p> * Optimized Constructor avoiding initial computation of comparison. */ public ObjectRange(Comparable smaller, Comparable larger, boolean reverse) { this(smaller, larger, (Boolean) reverse); } /** * Constructs a Range, computing reverse if not provided. When providing reverse, * 'smaller' must not be larger than 'larger'. * * @param smaller start of the range, must no be larger than to when reverse != null * @param larger end of the range, must be larger than from when reverse != null * @param reverse direction of the range. If null, causes direction to be computed (can be expensive). */ private ObjectRange(Comparable smaller, Comparable larger, Boolean reverse) { if (smaller == null) { throw new IllegalArgumentException("Must specify a non-null value for the 'from' index in a Range"); } if (larger == null) { throw new IllegalArgumentException("Must specify a non-null value for the 'to' index in a Range"); } if (reverse == null) { final boolean computedReverse = areReversed(smaller, larger); // ensure invariant from <= to if (computedReverse) { final Comparable temp = larger; larger = smaller; smaller = temp; } this.reverse = computedReverse; } else { this.reverse = reverse; } if (smaller instanceof Short) { smaller = ((Short) smaller).intValue(); } else if (smaller instanceof Float) { smaller = ((Float) smaller).doubleValue(); } if (larger instanceof Short) { larger = ((Short) larger).intValue(); } else if (larger instanceof Float) { larger = ((Float) larger).doubleValue(); } if (smaller instanceof Integer && larger instanceof Long) { smaller = ((Integer) smaller).longValue(); } else if (larger instanceof Integer && smaller instanceof Long) { larger = ((Integer) larger).longValue(); } /* areReversed() already does an implicit type compatibility check based on DefaultTypeTransformation.compareToWithEqualityCheck() for mixed classes but it is only invoked if reverse == null. So Object Range has to perform those type checks for consistency even when not calling compareToWithEqualityCheck(), and ObjectRange has to use the normalized value used in a successful comparison in compareToWithEqualityCheck(). Currently that means Chars and single-char Strings are evaluated as the char's charValue (an integer) when compared to numbers. So '7'..'9' should produce ['7', '8', '9'], whereas ['7'..9] and [7..'9'] should produce [55, 56, 57]. if classes match, or both numerical, no checks possible / necessary */ if (smaller.getClass() == larger.getClass() || (smaller instanceof Number && larger instanceof Number)) { this.from = smaller; this.to = larger; } else { // Convenience hack: try convert single-char strings to ints final Comparable tempfrom = normaliseStringType(smaller); final Comparable tempto = normaliseStringType(larger); // if after normalizing both are numbers, assume intended range was numbers if (tempfrom instanceof Number && tempto instanceof Number) { this.from = tempfrom; this.to = tempto; } else { // if convenience hack did not make classes match, // throw exception when starting with known class, and thus "from" cannot be advanced over "to". // Note if start is an unusual Object, it could have a next() method // that yields a Number or String to close the range final Comparable start = this.reverse ? larger : smaller; if (start instanceof String || start instanceof Number) { // starting with number will never reach a non-number, same for string throw new IllegalArgumentException("Incompatible Argument classes for ObjectRange " + smaller.getClass() + ", " + larger.getClass()); } // Since normalizing did not help, use original values at user's risk this.from = smaller; this.to = larger; } } checkBoundaryCompatibility(); } /** * throws IllegalArgumentException if to and from are incompatible, meaning they e.g. (likely) produce infinite sequences. * Called at construction time, subclasses may override cautiously (using only members to and from). */ protected void checkBoundaryCompatibility() { 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: different length"); } final int length = start.length(); int i; for (i = 0; i < length; i++) { if (start.charAt(i) != end.charAt(i)) { break; } } // strings must be equal except for the last character if (i < length - 1) { throw new IllegalArgumentException("Incompatible Strings for Range: String#next() will not reach the expected value"); } } } private static boolean areReversed(Comparable from, Comparable to) { try { return ScriptBytecodeAdapter.compareGreaterThan(from, to); } catch (IllegalArgumentException iae) { throw new IllegalArgumentException("Unable to create range due to incompatible types: " + from.getClass().getSimpleName() + ".." + to.getClass().getSimpleName() + " (possible missing brackets around range?)", iae); } } 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 <code>true</code> 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); } @Override public Comparable getFrom() { return from; } @Override public Comparable getTo() { return to; } @Override public boolean isReverse() { return reverse; } @Override public Comparable get(int index) { if (index < 0) { throw new IndexOutOfBoundsException("Index: " + index + " should not be negative"); } final StepIterator iter = new StepIterator(this, 1); Comparable value = iter.next(); for (int i = 0; i < index; i++) { if (!iter.hasNext()) { throw new IndexOutOfBoundsException("Index: " + index + " is too big for range: " + this); } value = iter.next(); } return value; } /** * 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); } protected int compareTo(Comparable first, Comparable second) { return DefaultGroovyMethods.numberAwareCompareTo(first, second); } /** * protection against calls from Groovy */ @SuppressWarnings("unused") private void setSize(int size) { throw new UnsupportedOperationException("size must not be changed"); } @Override public int size() { if (size == -1) { int tempsize = 0; if ((from instanceof Integer || from instanceof Long) && (to instanceof Integer || to instanceof Long)) { // let's fast calculate the size final BigInteger fromNum = new BigInteger(from.toString()); final BigInteger toNum = new BigInteger(to.toString()); final BigInteger sizeNum = toNum.subtract(fromNum).add(new BigInteger("1")); tempsize = sizeNum.intValue(); if (!BigInteger.valueOf(tempsize).equals(sizeNum)) { tempsize = Integer.MAX_VALUE; } } else if (from instanceof Character && to instanceof Character) { // let's fast calculate the size final char fromNum = (Character) from; final char toNum = (Character) to; tempsize = toNum - fromNum + 1; } else if (((from instanceof BigDecimal || from instanceof BigInteger) && to instanceof Number) || ((to instanceof BigDecimal || to instanceof BigInteger) && from instanceof Number)) { // 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(); tempsize = sizeNum.intValue(); if (!BigInteger.valueOf(tempsize).equals(sizeNum)) { tempsize = Integer.MAX_VALUE; } } else { // let's brute-force calculate the size by iterating start to end final Iterator<Comparable> iter = new StepIterator(this, 1); while (iter.hasNext()) { tempsize++; // integer overflow if (tempsize < 0) { break; } iter.next(); } } // integer overflow if (tempsize < 0) { tempsize = Integer.MAX_VALUE; } size = tempsize; } return size; } @Override public List<Comparable> subList(int fromIndex, int toIndex) { if (fromIndex < 0) { throw new IndexOutOfBoundsException("fromIndex = " + fromIndex); } if (fromIndex > toIndex) { throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); } if (fromIndex == toIndex) { return new EmptyRange<Comparable>(from); } // Performance detail: // not using get(fromIndex), get(toIndex) in the following to avoid stepping over elements twice final Iterator<Comparable> iter = new StepIterator(this, 1); Comparable toValue = iter.next(); int i = 0; for (; i < fromIndex; i++) { if (!iter.hasNext()) { throw new IndexOutOfBoundsException("Index: " + i + " is too big for range: " + this); } toValue = iter.next(); } final Comparable fromValue = toValue; for (; i < toIndex - 1; i++) { if (!iter.hasNext()) { throw new IndexOutOfBoundsException("Index: " + i + " is too big for range: " + this); } toValue = iter.next(); } return new ObjectRange(fromValue, toValue, reverse); } public String toString() { return reverse ? "" + to + ".." + from : "" + from + ".." + to; } @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. * * @see #containsWithinBounds(Object) */ @Override public boolean contains(Object value) { final Iterator<Comparable> iter = new StepIterator(this, 1); if (value == null) { return false; } while (iter.hasNext()) { if (DefaultTypeTransformation.compareEqual(value, iter.next())) return true; } return false; } @Override public void step(int step, Closure closure) { if (step == 0 && compareTo(from, to) == 0) { return; // from == to and step == 0, nothing to do, so return } final Iterator<Comparable> iter = new StepIterator(this, step); while (iter.hasNext()) { closure.call(iter.next()); } } /** * {@inheritDoc} */ @Override public Iterator<Comparable> iterator() { // non thread-safe iterator return new StepIterator(this, 1); } /** * Non-thread-safe iterator which lazily produces the next element only on calls of hasNext() or next() */ private static final class StepIterator implements Iterator<Comparable> { // actual step, can be +1 when desired step is -1 and direction is from high to low private final int step; private final ObjectRange range; private int index = -1; private Comparable value; private boolean nextFetched = true; private StepIterator(ObjectRange range, final int desiredStep) { if (desiredStep == 0 && range.compareTo(range.getFrom(), range.getTo()) != 0) { throw new GroovyRuntimeException("Infinite loop detected due to step size of 0"); } this.range = range; if (range.isReverse()) { step = -desiredStep; } else { step = desiredStep; } if (step > 0) { value = range.getFrom(); } else { value = range.getTo(); } } @Override public void remove() { range.remove(index); } @Override public Comparable next() { // not thread safe if (!hasNext()) { throw new NoSuchElementException(); } nextFetched = false; index++; return value; } @Override public boolean hasNext() { // not thread safe if (!nextFetched) { value = peek(); nextFetched = true; } return value != null; } private Comparable peek() { if (step > 0) { Comparable peekValue = value; for (int i = 0; i < step; i++) { peekValue = (Comparable) range.increment(peekValue); // handle back to beginning due to modulo incrementing if (range.compareTo(peekValue, range.from) <= 0) return null; } if (range.compareTo(peekValue, range.to) <= 0) { return peekValue; } } else { final int positiveStep = -step; Comparable peekValue = value; for (int i = 0; i < positiveStep; i++) { peekValue = (Comparable) range.decrement(peekValue); // handle back to beginning due to modulo decrementing if (range.compareTo(peekValue, range.to) >= 0) return null; } if (range.compareTo(peekValue, range.from) >= 0) { return peekValue; } } return null; } } @Override public List<Comparable> step(int step) { final IteratorClosureAdapter<Comparable> adapter = new IteratorClosureAdapter<Comparable>(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); } /** * if operand is a Character or a String with one character, return that character's int value. */ 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; } }