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.