Now that we have the nice new foreach loop in Java, the old-style loop looks ugly be comparison.
I like the way Python has a range()
generator that allows the foreach construct to iterate over a range of integers.
I have written a Range
class which allows this.
Comments please.
package highland.mark;
import java.util.Iterator;
/**
* A class to enable java's 'foreach' loop to accept a range.
* <p>
* This allows replacing the C style:
*
* <pre>
* <code>
* for (int i = 0; i < 10; i++) {...}
* </code>
* </pre>
*
* with:
*
* <pre>
* <code>
* for (int i : range(10)) {...}
* </code>
* </pre>
*
* Three versions of range() are provided allowing combinations of start, end,
* and step.
*
* @author Mark Thomas
* @version 1.0
*/
public final class Range implements Iterator<Integer>, Iterable<Integer> {
/**
* The next integer to be returned by the iterator.
*
*/
private int next;
/**
* The last integer to be returned will be (next - 1).
*/
private final int to;
/**
* The increment added to the value of next after each iteration.
*/
private final int step;
/**
* A Method to be used with the java 'foreach' loop.
* <p>
* Usage:
*
* <pre>
* <code>
* import static highland.mark.Range.range;
* ...
* for (int i : range(from, to, step)) {...}
* </code>
* </pre>
*
* @param from
* : int, first value returned.
* @param to
* : int, one more than last value returned.
* @param step
* : int, increment for each iteration (may be negative so long
* as <code>(to < step)</code>.
* @return An Iterable<Integer> which supplies an Iterator<Integer> which,
* on each iteration, returns integers from <code>from</code> to
* <code>(to - 1)</code> incrementing by <code>step</code> each
* time.
* @throws IllegalArgumentException
* if <code>step == 0</code> or <code>step</code> is the wrong
* sign.
*/
public static Iterable<Integer> range(int from, int to, int step)
throws IllegalArgumentException {
return new Range(from, to, step);
}
/**
* A Method to be used with the java 'foreach' loop.
* <p>
* Usage:
*
* <pre>
* <code>
* import static highland.mark.Range.range;
* ...
* for (int i : range(from, to)) {...}
* </code>
* </pre>
*
* @param from
* : int, first value returned.
* @param to
* : int, one more than last value returned.
* @return An Iterable<Integer> which supplies an Iterator<Integer> which,
* on each iteration, returns integers from <code>from</code> to
* <code>(to - 1)</code> incrementing by 1 each time.
* @throws IllegalArgumentException
* if <code>(to < from)<code>.
*/
public static Iterable<Integer> range(int from, int to)
throws IllegalArgumentException {
return Range.range(from, to, 1);
}
/**
* A Method to be used with the java 'foreach' loop.
* <p>
* Usage:
*
* <pre>
* <code>
* import static highland.mark.Range.range;
* ...
* for (int i : range(to)) {...}
* </code>
* </pre>
*
* @param to
* : int, one more than last value returned.
* @return An Iterable<Integer> which supplies an Iterator<Integer> which,
* on each iteration, returns integers from 0 to
* <code>(to - 1)</code> incrementing by 1 each time.
*/
public static Iterable<Integer> range(int to) {
return Range.range(0, to, 1);
}
/*
* (non-Javadoc) private constructor only used by the static range()
* methods.
*
* @param from : int, first value returned.
*
* @param to : int, one more than last value returned.
*
* @param step : int, increment for each iteration (may be negative so long
* as (to < step).
*
* @throws IllegalArgumentException if step == 0 or step is the wrong sign.
*/
private Range(int from, int to, int step) throws IllegalArgumentException {
if (step == 0) {
throw new IllegalArgumentException(
"The step argument cannot be zero");
}
if ((to - from) / step < 0) {
throw new IllegalArgumentException(
"The step argument has the wrong sign");
}
this.next = from;
this.to = to;
this.step = step;
}
/*
* (non-Javadoc)
*
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<Integer> iterator() {
return this;
}
/*
* (non-Javadoc)
*
* @see java.util.Iterator#hasNext()
*/
@Override
public boolean hasNext() {
return this.step < 0 ? this.to < this.next : this.next < this.to;
}
/*
* (non-Javadoc)
*
* @see java.util.Iterator#next()
*/
@Override
public Integer next() {
int value = this.next;
this.next += this.step;
return value;
}
/*
* (non-Javadoc)
*
* @see java.util.Iterator#remove()
*/
@Override
public void remove() throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"The iterator returned from range() does not support remove()");
}
}
Test code (testng):
package highland.mark;
import static highland.mark.Range.range;
import static org.testng.Assert.assertEquals;
import org.testng.annotations.Test;
public class RangeTest {
@Test
public void testRangeFullySpecified() {
String result = "";
for (int i : range(2, 19, 3)) {
result += i + " ";
}
assertEquals(result, "2 5 8 11 14 17 ");
}
@Test
public void testRangeBackWards() {
String result = "";
for (int i : range(19, 2, -3)) {
result += i + " ";
}
assertEquals(result, "19 16 13 10 7 4 ");
}
@Test
public void testRangeDefaultStep() {
String result = "";
for (int i : range(2, 9)) {
result += i + " ";
}
assertEquals(result, "2 3 4 5 6 7 8 ");
}
@Test
public void testRangeDefaultStepAndStart() {
String result = "";
for (int i : range(7)) {
result += i + " ";
}
assertEquals(result, "0 1 2 3 4 5 6 ");
}
@Test(expectedExceptions=IllegalArgumentException.class)
public void testwrongWay1() {
for (@SuppressWarnings("unused") int i : range(2, 19, -3)) {
// No-op;
}
}
@Test(expectedExceptions=IllegalArgumentException.class)
public void testwrongWay2() {
for (@SuppressWarnings("unused") int i : range(2, -19, 3)) {
// No-op;
}
}
@Test(expectedExceptions=IllegalArgumentException.class)
public void testZeroStep1() {
for (@SuppressWarnings("unused") int i : range(2, 19, 0)) {
// No-op;
}
}
@Test(expectedExceptions=IllegalArgumentException.class)
public void testZeroStep2() {
for (@SuppressWarnings("unused") int i : range(2, -19, 0)) {
// No-op;
}
}
}
In conclusion (after feedback)...
I appreciate the valuable feedback, and I have appended the refactored version below.
I would still like to know what people think of the idea itself!
package com.gmail.highland.mark;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* A class to enable java's 'foreach' loop to accept a range.
* <p>
* This allows replacing the C style:
* <pre><code>
* for (int i = 0; i < 10; i++) {...}
* </code></pre>
* with:
* <pre><code>
* for (int i : range(10)) {...}
* </code></pre>
* Three versions of range() are provided allowing combinations of start, end,
* and step.
* @author Mark Thomas
* @version 1.1
*/
public final class Range implements Iterator<Integer>, Iterable<Integer> {
private int next;
private final int from;
/**
* The iteration will stop the step before before reaching or passing
* <code>to</code>.
*/
private final int to;
private final int step;
/**
* Flag is set when iterator() is first called.
*
* @see iterator()
*/
private boolean used = false;
/**
* A Method to be used with the java 'foreach' loop.
* <p>
* Usage:
* <pre><code>
* import static highland.mark.Range.range;
* ...
* for (int i : range(from, to, step)) {...}
* </code></pre>
* @param from
* - first value returned.
* @param to
* - one more than last value returned.
* @param step
* - increment for each iteration (may be negative so long as
* {@code (to < step)}.
* @return the Iterable which supplies an Iterator which, on each iteration,
* returns integers from {@code from} to {@code (to - 1)}
* incrementing by {@code step} each time.
* @throws IllegalArgumentException
* if {@code step == 0} or {@code step} is the wrong sign.
*/
public static Iterable<Integer> range(int from, int to, int step)
throws IllegalArgumentException {
return new Range(from, to, step);
}
/**
* A Method to be used with the java 'foreach' loop.
* <p>
* Usage:
* <pre><code>
* import static highland.mark.Range.range;
* ...
* for (int i : range(from, to)) {...}
* </code></pre>
* @param from
* - first value returned.
* @param to
* - one more than last value returned.
* @return the Iterable which supplies an Iterator which, on each iteration,
* returns integers from {@code from} to {@code (to - 1)}
* incrementing by {@code 1} each time.
* @throws IllegalArgumentException
* if {@code to < from}.
*/
public static Iterable<Integer> range(int from, int to)
throws IllegalArgumentException {
return Range.range(from, to, 1);
}
/**
* A Method to be used with the java 'foreach' loop.
* <p>
* Usage:
* <pre><code>
* import static highland.mark.Range.range;
* ...
* for (int i : range(to)) {...}
* </code></pre>
* @param to
* - one more than last value returned.
* @return the Iterable which supplies an Iterator which, on each iteration,
* returns integers from {@code 0} to {@code (to - 1)}
* incrementing by {@code 1} each time.
*/
public static Iterable<Integer> range(int to) {
return Range.range(0, to, 1);
}
/*
* (non-Javadoc) private constructor only used by the static range()
* methods.
*
* @throws IllegalArgumentException if step == 0 or step is the wrong sign.
*/
private Range(int from, int to, int step) throws IllegalArgumentException {
if (step == 0) {
throw new IllegalArgumentException(
"The step argument cannot be zero");
}
if ((to - from) / step < 0) {
throw new IllegalArgumentException(
"The step argument has the wrong sign");
}
this.next = from;
this.from = from;
this.to = to;
this.step = step;
}
/*
* (non-Javadoc) private constructor only used by the iterator() method
*
* @param range : Range, object to be reset to starting values.
*/
private Range(Range range) {
this.next = range.from;
this.from = range.from;
this.to = range.to;
this.step = range.step;
}
@Override
public Iterator<Integer> iterator() {
if (!used) { // subsequent calls to iterator must return a fresh Range.
used = true;
return this;
}
return new Range(this);
}
@Override
public boolean hasNext() {
if (step < 0) {
return to < next;
} else {
return next < to;
}
}
@Override
public Integer next() {
if (!hasNext())
throw new NoSuchElementException("End of range exceeded.");
int value = this.next;
this.next += this.step;
return value;
}
/**
* @see java.util.Iterator#remove()
*/
@Override
public void remove() throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"The iterator returned from range() does not support remove()");
}
}
TestNG Unit Tests:
package com.gmail.highland.mark;
import static com.gmail.highland.mark.Range.range;
import static org.testng.Assert.assertEquals;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import org.testng.annotations.Test;
public class RangeTest {
private void verifyOutput(final Iterable<Integer> range, final Integer... expectedValues) {
final List<Integer> result = new ArrayList<>();
for (Integer value: range) {
result.add(value);
}
assertEquals(Arrays.asList(expectedValues), result);
}
@Test
public void fullySpecified() {
verifyOutput(range(2, 19, 3), 2, 5, 8, 11, 14, 17);
}
@Test
public void backWards() {
verifyOutput(range(19, 2, -3), 19, 16, 13, 10, 7, 4);
}
@Test
public void defaultStep() {
verifyOutput(range(2, 9), 2, 3, 4, 5, 6, 7, 8);
}
@Test
public void defaultStepAndStart() {
verifyOutput(range(7), 0, 1, 2, 3, 4, 5, 6);
}
@Test
public void iteratorReuse() {
// N.B. Not intended usage pattern
Iterable<Integer> rangeForReuse = range(7);
String result = "";
for (int i : rangeForReuse) {
result += i + " ";
}
for (int i : rangeForReuse) {
result += i + " ";
}
assertEquals(result, "0 1 2 3 4 5 6 0 1 2 3 4 5 6 ");
}
@Test(expectedExceptions=IllegalArgumentException.class)
public void whenWrongWay1() {
range(2, 19, -3);
}
@Test(expectedExceptions=IllegalArgumentException.class)
public void whenWrongWay2() {
range(2, -19, 3);
}
@Test(expectedExceptions=IllegalArgumentException.class)
public void whenZeroStep1() {
range(2, 19, 0);
}
@Test(expectedExceptions=IllegalArgumentException.class)
public void whenZeroStep2() {
range(2, -19, 0);
}
@Test(expectedExceptions=NoSuchElementException.class)
public void whenNextOutOfRange() {
// N.B. Not intended usage pattern
Iterator<Integer> iterator = range(2).iterator();
iterator.next(); // 0
iterator.next(); // 1
iterator.next(); // 2 - out of range
}
}
testFoo
when you use the@Test
annotation. Also, given that all tests are for theRange
class, I'd remove it from their names. – David Harkness Mar 27 at 22:58