Recently I was attempting to create a Range representing the difference between two
DateTime
values so I could assert in a test that a date time value fell into that range. I was using
Joda Time's
DateTime
class. The
java.util.Date
class is compatible with Groovy's
ObjectRange
becuase it's decorated with
next
and
previous
methods by the GDK. Joda's
DateTime
isn't, so I ended up doing this:
DateTime start = new DateTime()
DateTime end = start.plusYears(1)
Range range = start.millis..end.millis
I could have created a custom Range implementation but I'd have lost the option of using the
x..y
notation and I'm a sucker for brevity. I figured Groovy has an
IntRange
class so there's probably a
LongRange
as well. As it turns out there isn't and what I ended up with is an
ObjectRange
.
Fundamentally this works, although I did find something worth bearing in mind. When it came to my assertion I tried to do this:
static void assertInRange(Range expected, actual) {
if (!expected.contains(actual) {
fail("Expected value in range:<${expected}> but was:<${actual}>")
}
}
Simple enough, right? Except running the code caused the test to hang. Looking into the implementation of
ObjectRange
it became clear why. Groovy's
Range
interface extends
java.util.List
and that's where the
contains
method is specified. For compatibility with the
List
interface
ObjectRange
iterates over the values between its
from
and
to
properties using the
next
method I mentioned above. When
from
and
to
are
Long
instances, it turns out this iteration takes a while - on my 2.16GHz iMac the
contains
method takes over 4½ minutes on a 1 year range!
The
Range
interface also specifies a
containsWithinBounds
method and
ObjectRange
implements this by simply checking that the argument is >=
from
and <=
to
. Changing my assertion to use
containsWithinBounds
got rid of the long wait.
What
ObjectRange
is doing
does make perfect sense. Given two arbitrary objects that implement
Comparable
and the
next
and
previous
methods it needs to implement
contains
in a way that conforms to the
List
interface. When you think about how the GDK implements
next
and
previous
on
java.util.Date
for example you can see that there may be possible values of a given class that fall inside a range but would never be part of the
List
generated by repeatedly using
next
on the range's
from
property. This isn't true of
Long
, of course, but
ObjectRange
is a general purpose class. The
IntRange
class has a much optimised version of
contains
and so could a theoretical
LongRange
implementation.
Anyway, getting back to the example. After changing
contains
to
containsWithinBounds
the code ran much faster, unfortunately it also threw
java.lang.OutOfMemoryError: Java heap space
. It turns out this is down to how
GString
handles the
ObjectRange
value I inserted into it. It spots that the object it's got implements
List
and uses
InvokerHelper.toListString
on it. This will attempt to create a String in the form
"[0, 1, 2, 3...]"
. As you can imagine, building such a String is going to take some doing with over 31½ billion elements in the
List
, hence the heap space ran out.
A final working implementation of my assertion method is:
static void assertInRange(Range expected, actual) {
if (!expected.containsWithinBounds(actual)) {
fail "Expected:<${expected.toString()}> but was:<${actual}>"
}
}
I think going for the custom
Range
implementation in the first place might have actually been the right choice.