Wednesday, 29 October 2008

Joda DateTime Ranges

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.

No comments: