比较 Java 日期并将日期强制为特定时区
Comparing Java Dates and force a date to a particular time zone
我在比较日期时遇到问题。
我正在使用 Groovy 和 Spock 编写针对 Web 服务的集成测试。
测试是首先使用网络服务创建一个Thing
,然后立即再次调用以通过其ID 获取Thing
的详细信息。然后我想验证这个东西的 CreatedDate
是否大于一分钟前。
这里是一些JSON的调用
{
"Id":"696fbd5f-5a0c-4209-8b21-ea7f77e3e09d",
"CreatedDate":"2017-07-11T10:53:52"
}
因此,请注意日期字符串中没有时区信息;但我知道它是 UTC。
我是 Java(来自 .NET)的新手,对不同的日期类型有点迷惑。
这是我使用 Gson 反序列化的 class 的 Groovy 模型:
class Thing {
public UUID Id
public Date CreatedDate
}
反序列化工作正常。但是在非 UTC 时区运行的代码认为日期实际上是在本地时区。
我可以使用 Instant
class:
创建一个代表“1 分钟前”的变量
def aMinuteAgo = Instant.now().plusSeconds(-60)
这就是我尝试进行比较的方式:
rule.CreatedDate.toInstant().compareTo(aMinuteAgo) < 0
麻烦的是,运行时认为日期是当地时间。我似乎没有过载强制 .toInstant()
进入 UTC。
我尝试使用我认为更现代的 classes - 例如我模型中的 LocalDateTime
和 ZonedDateTime
而不是 Date
,但是 Gson反序列化效果不佳。
输入的String
只有日期和时间,没有时区信息,所以可以解析成LocalDateTime
再转成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();
您可以使用 ZonedDateTime
、OffsetDateTime
或 Instant
,因为它们都将包含等效的 UTC 日期和时间。要获得它们,您可以使用解串器 .
要检查此日期和当前日期之间的分钟数,您可以使用 java.time.temporal.ChronoUnit
:
ChronoUnit.MINUTES.between(instant, Instant.now());
这将 return instant
和当前 date/time 之间的分钟数。您还可以将其与 ZonedDateTime
或 OffsetDateTime
:
一起使用
ChronoUnit.MINUTES.between(z, ZonedDateTime.now());
ChronoUnit.MINUTES.between(odt, OffsetDateTime.now());
但是当你使用 UTC 时,Instant
更好(因为根据定义它是 "in UTC" - 实际上,Instant
代表一个时间点,自 epoch (1970-01-01T00:00Z
) 以来的纳秒并且没有 timezone/offset,所以你也可以认为 "it's always in UTC").
如果您确定日期始终采用 UTC,也可以使用 OffsetDateTime
。 ZonedDateTime
也可以,但是如果您不需要时区规则(跟踪 DST 规则等),那么 OffsetDateTime
是更好的选择。
另一个区别是Instant
只有自纪元(1970-01-01T00:00Z
)以来的纳秒数。如果需要日、月、年、时、分、秒等字段,最好使用ZonedDateTime
或OffsetDateTime
.
你也可以看看 API tutorial, which has a good explanation about the different types.
非常感谢您的评论,他们让我走上了一条好的道路。
为了其他遇到此问题的人的利益,这是我使用的代码。
修改模型class:
import java.time.LocalDateTime
class Thing {
public UUID Id
public LocalDateTime CreatedDate
}
Utils class(另外还包括 ZonedDateTime
的方法,因为那是我获得原始代码的地方。结果我可以让 LocalDateTime
为我工作。( setDateFormat
是为了支持 Date
在其他模型 class 中使用的对象,我不需要进行比较,尽管我可以看到自己很快就会弃用所有这些)。
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
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")
}
}
};
}
这里是进行比较的代码(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
用运算符做比较似乎更自然。当我明确将它用于 UTC 时,OffsetDateTime 效果很好。我只使用此模型对服务(它们本身实际上是在 .NET 中实现的)执行集成测试,因此不会在我的测试之外使用这些对象。
我在比较日期时遇到问题。
我正在使用 Groovy 和 Spock 编写针对 Web 服务的集成测试。
测试是首先使用网络服务创建一个Thing
,然后立即再次调用以通过其ID 获取Thing
的详细信息。然后我想验证这个东西的 CreatedDate
是否大于一分钟前。
这里是一些JSON的调用
{
"Id":"696fbd5f-5a0c-4209-8b21-ea7f77e3e09d",
"CreatedDate":"2017-07-11T10:53:52"
}
因此,请注意日期字符串中没有时区信息;但我知道它是 UTC。
我是 Java(来自 .NET)的新手,对不同的日期类型有点迷惑。
这是我使用 Gson 反序列化的 class 的 Groovy 模型:
class Thing {
public UUID Id
public Date CreatedDate
}
反序列化工作正常。但是在非 UTC 时区运行的代码认为日期实际上是在本地时区。
我可以使用 Instant
class:
def aMinuteAgo = Instant.now().plusSeconds(-60)
这就是我尝试进行比较的方式:
rule.CreatedDate.toInstant().compareTo(aMinuteAgo) < 0
麻烦的是,运行时认为日期是当地时间。我似乎没有过载强制 .toInstant()
进入 UTC。
我尝试使用我认为更现代的 classes - 例如我模型中的 LocalDateTime
和 ZonedDateTime
而不是 Date
,但是 Gson反序列化效果不佳。
输入的String
只有日期和时间,没有时区信息,所以可以解析成LocalDateTime
再转成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();
您可以使用 ZonedDateTime
、OffsetDateTime
或 Instant
,因为它们都将包含等效的 UTC 日期和时间。要获得它们,您可以使用解串器
要检查此日期和当前日期之间的分钟数,您可以使用 java.time.temporal.ChronoUnit
:
ChronoUnit.MINUTES.between(instant, Instant.now());
这将 return instant
和当前 date/time 之间的分钟数。您还可以将其与 ZonedDateTime
或 OffsetDateTime
:
ChronoUnit.MINUTES.between(z, ZonedDateTime.now());
ChronoUnit.MINUTES.between(odt, OffsetDateTime.now());
但是当你使用 UTC 时,Instant
更好(因为根据定义它是 "in UTC" - 实际上,Instant
代表一个时间点,自 epoch (1970-01-01T00:00Z
) 以来的纳秒并且没有 timezone/offset,所以你也可以认为 "it's always in UTC").
如果您确定日期始终采用 UTC,也可以使用 OffsetDateTime
。 ZonedDateTime
也可以,但是如果您不需要时区规则(跟踪 DST 规则等),那么 OffsetDateTime
是更好的选择。
另一个区别是Instant
只有自纪元(1970-01-01T00:00Z
)以来的纳秒数。如果需要日、月、年、时、分、秒等字段,最好使用ZonedDateTime
或OffsetDateTime
.
你也可以看看 API tutorial, which has a good explanation about the different types.
非常感谢您的评论,他们让我走上了一条好的道路。
为了其他遇到此问题的人的利益,这是我使用的代码。
修改模型class:
import java.time.LocalDateTime
class Thing {
public UUID Id
public LocalDateTime CreatedDate
}
Utils class(另外还包括 ZonedDateTime
的方法,因为那是我获得原始代码的地方。结果我可以让 LocalDateTime
为我工作。( setDateFormat
是为了支持 Date
在其他模型 class 中使用的对象,我不需要进行比较,尽管我可以看到自己很快就会弃用所有这些)。
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
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")
}
}
};
}
这里是进行比较的代码(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
用运算符做比较似乎更自然。当我明确将它用于 UTC 时,OffsetDateTime 效果很好。我只使用此模型对服务(它们本身实际上是在 .NET 中实现的)执行集成测试,因此不会在我的测试之外使用这些对象。