Making a Groovy class a range

In a recent post, I used the Date class in a range. This always jumps out at Java developers, who aren’t used to seeing it.

def now = new Date()
def then = now + 9
assert 10 == (now..then).size()

It’s really cool you can do that in Groovy. That example illustrates both the concept of a range as a collection of values, and that Groovy can add methods to an existing library class — in this case, java.util.Date.

As Groovy in Action (GinA) points out, any class can be used in a range if:

  • It has a next() method to determine the following element,
  • It has a previous() method to determine the preceding element, and
  • It implements the java.util.Comparable interface.

The Comparable interface, from Java, has a single method called compareTo(obj). It should return a positive value if the current object is greater than the argument, a negative number if the current object is less than the argument, and zero otherwise. Any class that implements the Comparable interface should also override the equals(obj) method in a consistent way, i.e., if compareTo returns zero, the equals method ought to return true.

There’s an example in GinA, but I decided to try to put something a bit more nontrivial together as an illustration.

I started with a class representing a train station:

class TrainStation {
    String name

    String toString() { name }

    boolean equals(Object o) {
        return this.name == o.name
    }

    int hashCode() { name.hashCode() }

    // ... more to come

So my station has a name field, and the equals and hashCode methods both delegate to that field.

Since I needed the TrainStation class to have a next() and a previous() method, I decided the easiest way to do that was to give the station a reference to the surrounding stations:

// ... from above, then
    TrainStation nextStation
    TrainStation previousStation

    TrainStation next() { nextStation }
    TrainStation previous() { previousStation }
// ...

So far, so good. I needed to initialize the next and previous station values, so I created a constant called END and assigned both pointers to that value in the constructor.

//...
    public static final TrainStation END = new TrainStation(name:'END')

    TrainStation() {
        nextStation = END
        previousStation = END
    }

(If this is starting to resemble a “circular doubly-linked list” to you, the resemblance is purely intentional.)

The hard part was implementing Comparable. If a station is in the “next” direction, it’s considered greater, and if it’s in the “previous” direction, it should be less than the current station. The problem is, if all I have available is pointers to the immediate neighbors, how can I compare to stations that could be many steps away?

There are probably lots of ways to do it, but the technique that occurred to me was recursion. I also needed to check in both directions, because I don’t know whether the supplied argument will be greater, lesser, or equal. After a lot of playing around, I eventually implemented it this way.

// ...
	int compareTo(Object o) {
		boolean greater = false
		boolean lesser = false
		
		greater = isGreater(o)
		if (greater) { return 1 } 
		
		lesser = isLesser(o)
		if (lesser) { return -1 }

		return 0
	}

	boolean isLesser(TrainStation ts) {
		if (this.equals(END)) { return false }

		if (this.next().equals(END)) { return false }

		if (this.next().equals(ts)) { return true }

		return nextStation.isLesser(ts)
	}
	
	boolean isGreater(TrainStation ts) {
		if (this.equals(END)) { return false }

		if (this.previous().equals(END)) { return false }
		
		if (this.previous().equals(ts)) { return true }

		return this.previous().isGreater(ts)
	}

In each direction, I check the current node against its neighbor. If that isn’t conclusive, then I move from the current node to the next or previous node and check again, continuing until either I reach the desired node or I hit the end of the line.

Now, to test all this, I needed a Track class (if the TrainStation is a node in a linked list, the Track class is the linked list itself). The only complicated part of that is the method to add a new TrainStation, as shown below.

import java.util.List
import java.util.LinkedList

public class Track {
	List<TrainStation> stations = new LinkedList<TrainStation>()
	
	Track() {
                // all new stations need a boundary on each end
		stations.add(TrainStation.END)
	}

	void addStation(TrainStation ts) {
                // play with pointers to append the new station
		TrainStation last = stations.last()
		ts.previousStation = last
		ts.nextStation = TrainStation.END
		last.nextStation = ts

                // might as well store the stations, too
		stations.add(ts)
	}
	
	def visitStations(TrainStation from, TrainStation to) {
		def result = []
                // Whoo hoo!  TrainStations are a range!
		for (station in (from..to)) {
			result << station
		}
		return result
	}
	
	String toString() {
		return stations.toString()
	}
}
&#91;/sourcecode&#93;

The <code>visitStations</code> method is where the <code>TrainStation</code> class, at long last, is used in a range.  A client asks the track to visit stations from a beginning station to an ending station.  The intermediate stations are all added to an <code>ArrayList</code> (i.e., the <code>[]</code> value), which is then returned to the client.

If it's not tested, it's broken.  I'll omit the <code>TrainStationTests</code> class, which just verified that <code>next()</code>, <code>previous()</code>, and <code>compareTo()</code> worked, but here's the <code>TrackTests</code> class.

public class TrackTests extends GroovyTestCase {
	Track acela
	
	TrainStation bos = new TrainStation(name:'Boston')
	TrainStation nh = new TrainStation(name:'New Haven')
	TrainStation ny = new TrainStation(name:'New York')
	TrainStation ph = new TrainStation(name:'Philadelphia')
	TrainStation dc = new TrainStation(name:'Washington')
	
	void setUp() throws Exception{
		acela = new Track()
		acela.addStation(bos)
		acela.addStation(nh)
		acela.addStation(ny)
		acela.addStation(ph)
		acela.addStation(dc)
	}
	
	void testVisitOneStation() {
		def expected = [bos].toArray()
		def result = acela.visitStations(bos,bos).toArray()
		assertArrayEquals(expected,result)
	}
	
	void testVisitTwoStations() {
		def expected = [bos,nh].toArray()
		def result = acela.visitStations(bos,nh).toArray()
		assertArrayEquals(expected,result)
	}
	
	void testVisitManyStations(){
		def acela_sb = [bos, nh, ny, ph, dc]
		def result = acela.visitStations(bos,dc)
		assertArrayEquals(acela_sb.toArray(),result.toArray())
		
		def acela_nb = [dc, ph, ny, nh, bos]
		result = acela.visitStations(dc,bos)
		assertArrayEquals(acela_nb.toArray(),result.toArray())
	}	
}

Using GroovyTestCase gives me the convenient assertArrayEquals method, which is also cool.

So the result is that I have a new class that I can use in a range. Maybe the GinA authors were right not keep their example simpler than this, but I had fun. ­čÖé

As an aside, it should be noted that computing the size of a range is not an efficient operation. The implementation requires the loop to traverse the range, element by element, using the next() method, counting elements as it goes. So a date range spanning years is going to look at every single day in between. It’s worth keeping that in mind when you want to compute how many elements are traversed.

%d bloggers like this: