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.
[sourcecode language=”java”]
def now = new Date()
def then = now + 9
assert 10 == (now..then).size()
[/sourcecode]
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:
[sourcecode language=”java”]
class TrainStation {
String name
String toString() { name }
boolean equals(Object o) {
return this.name == o.name
}
int hashCode() { name.hashCode() }
// … more to come
[/sourcecode]
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:
[sourcecode language=”java”]
// … from above, then
TrainStation nextStation
TrainStation previousStation
TrainStation next() { nextStation }
TrainStation previous() { previousStation }
// …
[/sourcecode]
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.
[sourcecode language=”java”]
//…
public static final TrainStation END = new TrainStation(name:’END’)
TrainStation() {
nextStation = END
previousStation = END
}
[/sourcecode]
(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.
[sourcecode language=”java”]
// …
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)
}
[/sourcecode]
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.
[sourcecode language=”java”]
import java.util.List
import java.util.LinkedList
public class Track {
List
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()
}
}
[/sourcecode]
The visitStations
method is where the TrainStation
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 ArrayList
(i.e., the []
value), which is then returned to the client.
If it’s not tested, it’s broken. I’ll omit the TrainStationTests
class, which just verified that next()
, previous()
, and compareTo()
worked, but here’s the TrackTests
class.
[sourcecode language=”java”]
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())
}
}
[/sourcecode]
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.
2 responses to “Making a Groovy class a range”
What is that code snippet plugin that you use????
Pretty cool, isn’t it?
Actually, it’s a built-in WordPress plugin. Just wrap your code in [sourcecode language=’java’]…[/sourcecode] and it works.
That statement probably triggered the plugin, so let me rephrase. Wrap your code in “open-square-bracket” ‘sourcecode’ language=’java’ “close-square-bracket”, which then terminates with “open-square-bracket” “forward-slash” ‘sourcecode’ “close-square-bracket”.
The plugin info page is located http://wordpress.org/extend/plugins/syntaxhighlighter2/other_notes/ .