Reputation: 740
I am having trouble doing a date comparison.
I am using Groovy and Spock to write an integration test against a web service.
The test is to first use the web service to create a Thing
, then immediately make another call to get the details of the Thing
by its ID. I then want to verify that the CreatedDate
of the thing is greater than a minute ago.
Here is some JSON of the call
{
"Id":"696fbd5f-5a0c-4209-8b21-ea7f77e3e09d",
"CreatedDate":"2017-07-11T10:53:52"
}
So, note no timezone information in the date string; but I know it is UTC.
I'm new to Java (from .NET) and a bit bamboozled by the different date types.
This is my Groovy model of the class, which I use Gson to deserialize:
class Thing {
public UUID Id
public Date CreatedDate
}
The deserialization works fine. But the code, which runs in a non-UTC time zone thinks that the date is actually in the local time zone.
I can create a variable representing "1 minute ago" using the Instant
class:
def aMinuteAgo = Instant.now().plusSeconds(-60)
And this is how I am trying to do the comparison:
rule.CreatedDate.toInstant().compareTo(aMinuteAgo) < 0
The trouble is, the runtime thinks that the date is local time. There appears to be no overload for me to force .toInstant()
into UTC.
I've tried using what I understand to be more modern classes - such as LocalDateTime
and ZonedDateTime
in my model instead of Date
, however Gson doesn't play nice with the deserialization.
Upvotes: 1
Views: 4707
Reputation: 740
Thanks so much for the comments, they put me on a good path.
For the benefit of anyone else who is stuck with this problem, here is the code I used.
Modified model class:
import java.time.LocalDateTime
class Thing {
public UUID Id
public LocalDateTime CreatedDate
}
Utils class (includes method for ZonedDateTime
in addition, as that's where I got the original code. It turned out I could make LocalDateTime
work for me though. (The setDateFormat
is to support Date
objects used in other model classes where I don't need to do comparisons, although I can see myself deprecating all that soon).
class Utils {
static Gson UtilGson = new GsonBuilder()
.registerTypeAdapter(ZonedDateTime.class, GsonHelper.ZDT_DESERIALIZER)
.registerTypeAdapter(LocalDateTime.class, GsonHelper.LDT_DESERIALIZER)
.registerTypeAdapter(OffsetDateTime.class, GsonHelper.ODT_DESERIALIZER)
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
.create();
// From https://stackoverflow.com/a/36418842/276036
static class GsonHelper {
public static final JsonDeserializer<ZonedDateTime> ZDT_DESERIALIZER = new JsonDeserializer<ZonedDateTime>() {
@Override
public ZonedDateTime deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive();
try {
// if provided as String - '2011-12-03T10:15:30+01:00[Europe/Paris]'
if(jsonPrimitive.isString()){
return ZonedDateTime.parse(jsonPrimitive.getAsString(), DateTimeFormatter.ISO_ZONED_DATE_TIME);
}
// if provided as Long
if(jsonPrimitive.isNumber()){
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(jsonPrimitive.getAsLong()), ZoneId.systemDefault());
}
} catch(RuntimeException e){
throw new JsonParseException("Unable to parse ZonedDateTime", e);
}
throw new JsonParseException("Unable to parse ZonedDateTime");
}
};
public static final JsonDeserializer<LocalDateTime> LDT_DESERIALIZER = new JsonDeserializer<LocalDateTime>() {
@Override
public LocalDateTime deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive();
try {
// if provided as String - '2011-12-03T10:15:30'
if(jsonPrimitive.isString()){
return LocalDateTime.parse(jsonPrimitive.getAsString(), DateTimeFormatter.ISO_DATE_TIME);
}
// if provided as Long
if(jsonPrimitive.isNumber()){
return LocalDateTime.ofInstant(Instant.ofEpochMilli(jsonPrimitive.getAsLong()), ZoneId.systemDefault());
}
} catch(RuntimeException e){
throw new JsonParseException("Unable to parse LocalDateTime", e);
}
throw new JsonParseException("Unable to parse LocalDateTime");
}
public static final JsonDeserializer<OffsetDateTime> ODT_DESERIALIZER = new JsonDeserializer<OffsetDateTime>() {
@Override
public OffsetDateTime deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive()
try {
// if provided as String - '2011-12-03T10:15:30' (i.e. no timezone information e.g. '2011-12-03T10:15:30+01:00[Europe/Paris]')
// We know our services return UTC dates without specific timezone information so can do this.
// But if, in future we have a different requirement, we'll have to review.
if(jsonPrimitive.isString()){
LocalDateTime localDateTime = LocalDateTime.parse(jsonPrimitive.getAsString());
return localDateTime.atOffset(ZoneOffset.UTC)
}
} catch(RuntimeException e){
throw new JsonParseException("Unable to parse OffsetDateTime", e)
}
throw new JsonParseException("Unable to parse OffsetDateTime")
}
}
};
}
And here is the code that does the comparison (it's Spock/Groovy):
// ... first get the JSON text from the REST call
when:
text = response.responseBody
def thing = Utils.UtilGson.fromJson(text, Thing.class)
def now = OffsetDateTime.now(ZoneOffset.UTC)
def aMinuteAgo = now.plusSeconds(-60)
then:
thing.CreatedDate > aMinuteAgo
thing.CreatedDate < now
It seems more natural to do comparisons with operators. And OffsetDateTime works well when I explicitly use it for UTC. I only use this model to perform integration tests against the services (which themselves are actually implemented in .NET) so the objects are not going to be used outside of my tests.
Upvotes: 1
Reputation:
The input String
has only date and time, and no timezone information, so you can parse it to a LocalDateTime
and then convert it to UTC:
// parse date and time
LocalDateTime d = LocalDateTime.parse("2017-07-11T10:53:52");
// convert to UTC
ZonedDateTime z = d.atZone(ZoneOffset.UTC);
// or
OffsetDateTime odt = d.atOffset(ZoneOffset.UTC);
// convert to Instant
Instant instant = z.toInstant();
You can either use the ZonedDateTime
, OffsetDateTime
or Instant
, as all will contain the equivalent date and time in UTC. To get them, you can use a deserializer, as linked in the comments.
To check how many minutes are between this date and the current date, you can use java.time.temporal.ChronoUnit
:
ChronoUnit.MINUTES.between(instant, Instant.now());
This will return the number of minutes between instant
and the current date/time. You can also use it with a ZonedDateTime
or with a OffsetDateTime
:
ChronoUnit.MINUTES.between(z, ZonedDateTime.now());
ChronoUnit.MINUTES.between(odt, OffsetDateTime.now());
But as you're working with UTC, an Instant
is better (because it's "in UTC" by definition - actually, the Instant
represents a point in time, the number of nanoseconds since epoch (1970-01-01T00:00Z
) and has no timezone/offset, so you can also think that "it's always in UTC").
You can also use OffsetDateTime
if you're sure the dates will always be in UTC. ZonedDateTime
also works, but if you don't need timezone rules (keep track of DST rules and so on), then OffsetDateTime
is a better choice.
Another difference is that Instant
has only the number of nanoseconds since epoch (1970-01-01T00:00Z
). If you need fields like day, month, year, hour, minutes, seconds, etc, it's better to use ZonedDateTime
or OffsetDateTime
.
You can also take a look at the API tutorial, which has a good explanation about the different types.
Upvotes: 3