I’m at SpringOne2GX this week, but this morning before my first presentation I noticed the following in my Twitter feed:
First of all, I think that’s the first time anything of mine has ever appeared in @DailyGrailsTip, so yay for me. On the other hand, the link is to a script that isn’t terribly self explanatory. At the risk of spoiling part of my Advanced Groovy Tips and Tricks talk, I thought I’d discuss it here.
The basic idea is that any Groovy class can be made into a range if it:
- implements
java.util.Comparable
- has a
next
method - has a
previous
method
My example uses a TrainStation
class, which forms a doubly-linked list when I place them along a track. So part of the implementation is trivial:
[sourcecode language=”groovy”]
class TrainStation implements Comparable<TrainStation> {
// Links to next and previous stations
TrainStation next
TrainStation previous
TrainStation next() { next }
TrainStation previous() { previous }
@Override // implement Comparable
int compareTo(TrainStation ts) {
// Hmm. What to do here?
}
}
[/sourcecode]
The TrainStation
has a reference to the next one and the previous one, and the implementations of next
and previous
just return them. (I considered making a wrapper class called Track
, but kept it simple here.)
The question is, how am I going to implement Comparable
? Since one important characteristic of train stations is their physical location, I decided to use latitude and longitude, and made the comparison based on latitude.
The result is:
[sourcecode language=”groovy”]
import java.text.NumberFormat
class TrainStation implements Comparable<TrainStation> {
// Links to next and previous stations
TrainStation next
TrainStation previous
// location attributes
String city
String state
// set by Geocoder service
BigDecimal latitude
BigDecimal longitude
TrainStation next() { next }
TrainStation previous() { previous }
@Override
int compareTo(TrainStation ts) {
this.latitude <=> ts.latitude
}
String toString() {
NumberFormat nf = NumberFormat.instance
"$city, $state \t (${nf.format(latitude)}, ${nf.format(longitude)})"
}
}
[/sourcecode]
Each train station has a city
and state
, which I set, and a latitude
and longitude
, which are computed from a geocoder. My geocoder is based on Google’s RESTful web service, which I’ve used many times before:
[sourcecode language=”groovy”]
class Geocoder {
public static final String BASE = ‘http://maps.google.com/maps/api/geocode/xml?’
void fillInLatLng(TrainStation station) {
String encoded =
[station.city, station.state].collect {
URLEncoder.encode(it,’UTF-8′)
}.join(‘,’)
String qs = [sensor:false, address: encoded].collect { it }.join(‘&’)
def response = new XmlSlurper().parse("$BASE$qs")
station.latitude = response.result[0].geometry.location.lat.toBigDecimal()
station.longitude = response.result[0].geometry.location.lng.toBigDecimal()
}
}
[/sourcecode]
The fillInLatLng
method takes a train station and URL encodes its city and state, so that they can be sent as part of a URL. It then builds a query string by converting the map entries in the form “key:value” into list entries in the form “key=value”.
(Note: I used to write that as [a:1, b:2].collect { k,v -> "$k=$v" }
. Then Paul King pointed out that the default toString
method on Map.Entry
is in fact “key=value”. Therefore I can just write [a:1, b:2].collect { it }
and it does the same thing. Live and learn.)
After building the query string, I use the parse
method from XmlSlurper
to access the web service, download the resulting XML, parse it into a DOM tree, and hand me the root. The rest is just walking the tree, keeping in mind that sometimes Google returns more than one request, so I made sure to use the first one only by writing result[0]
.
To use the new class, I need to create some train station and set their next and previous properties. Then I can use them in a range. Here’s a script to do all that:
[sourcecode language=”groovy”]
// Amtrak, NE corridor (mostly)
TrainStation was = new TrainStation(city:’Washington’, state:’DC’)
TrainStation bal = new TrainStation(city:’Baltimore’, state:’MD’)
TrainStation wil = new TrainStation(city:’Wilmington’, state:’DE’)
TrainStation phl = new TrainStation(city:’Philadelphia’, state:’PA’)
TrainStation nwk = new TrainStation(city:’Newark’, state:’NJ’)
TrainStation nyc = new TrainStation(city:’New York’, state:’NY’)
TrainStation nhv = new TrainStation(city:’New Haven’, state:’CT’)
TrainStation pvd = new TrainStation(city:’Providence’, state:’RI’)
TrainStation bos = new TrainStation(city:’Boston’, state:’MA’)
// Arrange the stations on the track
was.next = bal; bal.previous = was
bal.next = wil; wil.previous = bal
wil.next = phl; phl.previous = wil
phl.next = nwk; nwk.previous = phl
nwk.next = nyc; nyc.previous = nwk
nyc.next = nhv; nhv.previous = nyc
nhv.next = pvd; pvd.previous = nhv
pvd.next = bos; bos.previous = pvd
def ne_corridor = [was, bal, wil, phl, nwk, nyc, nhv, pvd, bos]
// Fill in all the latitudes and longitudes
Geocoder geo = new Geocoder()
ne_corridor.each { station ->
geo.fillInLatLng(station)
}
// range heading north
println ‘\nNorthbound from WAS to BOS’
(was..bos).each { println it }
// range heading south
println ‘\nSouthbound from BOS to WAS’
(bos..was).each { println it }
// subrange from nyc to bos
println ‘\nNYC to BOS’
(nyc..bos).each { println it.city }
[/sourcecode]
The resulting output is:
Northbound from WAS to BOS
Washington, DC (38.907, -77.036)
Baltimore, MD (39.29, -76.612)
Wilmington, DE (39.746, -75.547)
Philadelphia, PA (39.952, -75.164)
Newark, NJ (40.736, -74.172)
New York, NY (40.714, -74.006)
New Haven, CT (41.308, -72.928)
Providence, RI (41.824, -71.413)
Boston, MA (42.358, -71.06)
Southbound from BOS to WAS
Boston, MA (42.358, -71.06)
Providence, RI (41.824, -71.413)
New Haven, CT (41.308, -72.928)
New York, NY (40.714, -74.006)
Newark, NJ (40.736, -74.172)
Philadelphia, PA (39.952, -75.164)
Wilmington, DE (39.746, -75.547)
Baltimore, MD (39.29, -76.612)
Washington, DC (38.907, -77.036)
NYC to BOS
New York
New Haven
Providence
Boston
There you have it. I’ve taken an arbitrary class, TrainStation
, and enabled it to be used in a Groovy range. This whole demo is part of the code I use in my “Advanced Groovy” talk, stored in my GitHub repository at https://github.com/kousen/AdvancedGroovy .
Quick book update: The ebook version of Making Java Groovy should be released today. The print book should be available from Manning on 9/16 or 9/17. The remaining mobile formats (epub and mobi) will be released on 9/27. I’m still hoping to have some physical copies here at the conference to give away, but I haven’t seen them yet.
Btw, it’s wonderful seeing so many of the Groovy and Grails team members at the conference. I seriously doubt, however, that any of them took the train to get here. 🙂
Leave a Reply