Java 8 Time API - ZonedDateTime - 解析时指定默认 ZoneId

Posted

技术标签:

【中文标题】Java 8 Time API - ZonedDateTime - 解析时指定默认 ZoneId【英文标题】:Java 8 Time API - ZonedDateTime - specify default ZoneId when parsing 【发布时间】:2018-02-23 10:18:14 【问题描述】:

我正在尝试编写一个通用方法来返回ZonedDateTime,给定日期为String 及其格式。

如果没有在日期String 中指定,我们如何使ZonedDateTime 使用默认的ZoneId

可以用java.util.Calendar完成,但我想使用Java 8 time API。

This question here 使用的是固定时区。我将格式指定为参数。日期及其格式都是 String 参数。更通用。

下面的代码和输出:

public class DateUtil 
    /** Convert a given String to ZonedDateTime. Use default Zone in string does not have zone.  */
    public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) 
        //use java.time from java 8
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
        ZonedDateTime zonedDateTime = ZonedDateTime.parse(date, formatter);
        return zonedDateTime;
    

    public static void main(String args[]) 
        DateUtil dateUtil = new DateUtil();
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00+0530", "yyyy-MM-dd HH:mm:ssZ"));
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00", "yyyy-MM-dd HH:mm:ss"));
    

输出

2017-09-14T15:00+05:30
Exception in thread "main" java.time.format.DateTimeParseException: Text '2017-09-14 15:00:00' could not be parsed: Unable to obtain ZonedDateTime from TemporalAccessor: ,ISO resolved to 2017-09-14T15:00 of type java.time.format.Parsed
    at java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1920)
    at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1855)
    at java.time.ZonedDateTime.parse(ZonedDateTime.java:597)
    at com.nam.sfmerchstorefhs.util.DateUtil.parseToZonedDateTime(DateUtil.java:81)
    at com.nam.sfmerchstorefhs.util.DateUtil.main(DateUtil.java:97)
Caused by: java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: ,ISO resolved to 2017-09-14T15:00 of type java.time.format.Parsed
    at java.time.ZonedDateTime.from(ZonedDateTime.java:565)
    at java.time.format.Parsed.query(Parsed.java:226)
    at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
    ... 3 more
Caused by: java.time.DateTimeException: Unable to obtain ZoneId from TemporalAccessor: ,ISO resolved to 2017-09-14T15:00 of type java.time.format.Parsed
    at java.time.ZoneId.from(ZoneId.java:466)
    at java.time.ZonedDateTime.from(ZonedDateTime.java:553)
    ... 5 more

【问题讨论】:

How to parse ZonedDateTime with default zone?的可能重复 感谢大家的回复。非常有帮助。很遗憾,我只能接受一个答案。 【参考方案1】:

ZonedDateTime 需要构建时区或偏移量,而第二个输入没有它。 (它只包含一个日期和时间)。

因此,您需要检查是否可以构建ZonedDateTime,如果不是,则必须为其选择任意时区(因为输入没有指示所使用的时区是什么,你必须选择一个来使用)。

另一种方法是首先尝试创建ZonedDateTime,如果不可能,则创建LocalDateTime 并将其转换为时区:

public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) 
    // use java.time from java 8
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
    ZonedDateTime zonedDateTime = null;
    try 
        zonedDateTime = ZonedDateTime.parse(date, formatter);
     catch (DateTimeException e) 
        // couldn't parse to a ZoneDateTime, try LocalDateTime
        LocalDateTime dt = LocalDateTime.parse(date, formatter);

        // convert to a timezone
        zonedDateTime = dt.atZone(ZoneId.systemDefault());
    
    return zonedDateTime;

在上面的代码中,我使用的是ZoneId.systemDefault(),它获取JVM默认时区,但是这个can be changed without notice, even at runtime,所以最好总是明确说明你正在使用哪个.

API 使用IANA timezones names(始终采用Region/City 格式,如America/Sao_PauloEurope/Berlin)。 避免使用三个字母的缩写(如CSTPST),因为它们是ambiguous and not standard。

您可以致电ZoneId.getAvailableZoneIds() 获取可用时区列表(并选择最适合您系统的时区)。

如果您想使用特定时区,只需使用ZoneId.of("America/New_York")(或ZoneId.getAvailableZoneIds() 返回的任何其他有效名称,纽约只是一个示例)而不是ZoneId.systemDefault()


另一种选择是使用parseBest() method,它会尝试创建一个合适的日期对象(使用TemporalQuery 的列表),直到它创建您想要的类型:

public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) 
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);

    // try to create a ZonedDateTime, if it fails, try LocalDateTime
    TemporalAccessor parsed = formatter.parseBest(date, ZonedDateTime::from, LocalDateTime::from);

    // if it's a ZonedDateTime, return it
    if (parsed instanceof ZonedDateTime) 
        return (ZonedDateTime) parsed;
    
    if (parsed instanceof LocalDateTime) 
        // convert LocalDateTime to JVM default timezone
        LocalDateTime dt = (LocalDateTime) parsed;
        return dt.atZone(ZoneId.systemDefault());
    

    // if it can't be parsed, return null or throw exception?
    return null;

在这种情况下,我只使用了ZonedDateTime::fromLocalDateTime::from,因此格式化程序将首先尝试创建ZonedDateTime,如果不可能,则尝试创建LocalDateTime

然后我检查返回的类型是什么并相应地执行操作。 您可以添加任意数量的类型(所有主要类型,例如LocalDateLocalTimeOffsetDateTime 等,有一个与parseBest 一起使用的from 方法 - 如果您也可以create your own custom TemporalQuery你想要,但我认为内置方法对于这种情况已经足够了)。


夏令时

使用atZone() 方法将LocalDateTime 转换为ZonedDateTime 时,有一些关于Daylight Saving Time (DST) 的棘手情况。

我将以我居住的时区 (America/Sao_Paulo) 为例,但这可能发生在任何有夏令时的时区。

在圣保罗,DST 于 2016 年 10 月 16 日th 开始:在午夜,时钟从午夜到凌晨 1 点向前移动 1 小时(偏移量从 @987654359 @ 到 -02:00)。因此,该时区不存在 00:00 到 00:59 之间的所有当地时间(您也可以认为时钟从 23:59:59.999999999 直接更改为 01:00)。如果我在此间隔内创建本地日期,则会将其调整为下一个有效时刻:

ZoneId zone = ZoneId.of("America/Sao_Paulo");

// October 16th 2016 at midnight, DST started in Sao Paulo
LocalDateTime d = LocalDateTime.of(2016, 10, 16, 0, 0, 0, 0);
ZonedDateTime z = d.atZone(zone);
System.out.println(z);// adjusted to 2017-10-15T01:00-02:00[America/Sao_Paulo]

DST 结束时:2017 年 2 月 19 日th 午夜,时钟后移 1 小时,从午夜到18th(偏移量从-02:00 更改为-03:00)。因此,从 23:00 到 23:59 的所有当地时间都存在 两次(在两个偏移量中:-03:00-02:00),您必须决定要使用哪一个。 默认使用夏令时结束前的偏移量,但可以使用withLaterOffsetAtOverlap()方法获取夏令时结束后的偏移量:

// February 19th 2017 at midnight, DST ends in Sao Paulo
// local times from 23:00 to 23:59 at 18th exist twice
LocalDateTime d = LocalDateTime.of(2017, 2, 18, 23, 0, 0, 0);
// by default, it gets the offset before DST ends
ZonedDateTime beforeDST = d.atZone(zone);
System.out.println(beforeDST); // before DST end: 2018-02-17T23:00-02:00[America/Sao_Paulo]

// get the offset after DST ends
ZonedDateTime afterDST = beforeDST.withLaterOffsetAtOverlap();
System.out.println(afterDST); // after DST end: 2018-02-17T23:00-03:00[America/Sao_Paulo]

请注意,夏令时结束前后的日期有不同的偏移量(-02:00-03:00)。如果您正在使用具有 DST 的时区,请记住,这些极端情况可能会发生。

【讨论】:

【参考方案2】:

java.time 库中几乎没有默认值,这主要是一件好事 - 所见即所得,期间。

我建议如果您的日期字符串不包含区域 - 它是 LocalDateTime,并且不能是 ZonedDateTime,这就是您得到的异常的含义(即使措辞由于过于灵活的代码结构)。

如果您知道该模式没有区域信息,我的主要建议是解析为本地日期时间。

但是,如果您真的必须这样做,这里有另一种方法来做您想做的事情(一种不使用异常来控制流程的替代解决方案):

TemporalAccessor parsed = f.parse(string);
if (parsed.query(TemporalQueries.zone()) == null) 
  parsed = f.withZone(ZoneId.systemDefault()).parse(string);

return ZonedDateTime.from(parsed);

这里我们使用中间解析结果来判断字符串是否包含Zone信息,如果没有,我们再次解析(使用相同的字符串,但使用不同的printer-parser),以便它这次包含一个区域。

或者,您可以创建此类,这将使您免于第二次解析,并且应该允许您解析分区日期时间,假设所有其他字段都在那里:

class TemporalWithZone implements TemporalAccessor 
  private final ZoneId zone;
  private final TemporalAccessor delegate;
  public TemporalWithZone(TemporalAccessor delegate, ZoneId zone) 
    this.delegate = requireNonNull(delegate);
    this.zone = requireNonNull(zone);
  

  <delegate methods: isSupported(TemporalField), range(TemporalField), getLong(TemporalField)>

  public <R> R query(TemporalQuery<R> query) 
    if (query == TemporalQueries.zone() || query == TemporalQueries.zoneId()) 
      return (R) zone;
    
    return delegate.query(query);
  

【讨论】:

【参考方案3】:

根据 Java 8 ZonedDateTime 实现,您无法在 ZonedDateTime 中解析没有区域的日期。

为了解决给定的问题,您必须使用 try catch 以防出现任何异常,它会考虑默认时区。

请找到修改后的程序如下:

public class DateUtil 
     /** Convert a given String to ZonedDateTime. Use default Zone in string does not have zone.  */
    public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) 
        //use java.time from java 8
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
        ZonedDateTime zonedDateTime = null;
        try 
            zonedDateTime = ZonedDateTime.parse(date, formatter);
         catch (DateTimeException e) 
            // If date doesn't contains Zone then parse with LocalDateTime 
            LocalDateTime localDateTime = LocalDateTime.parse(date, formatter);
            zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
        
        return zonedDateTime;
    

    public static void main(String args[]) 
        DateUtil dateUtil = new DateUtil();
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00+0530", "yyyy-MM-dd HH:mm:ssZ"));
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00", "yyyy-MM-dd HH:mm:ss"));
    

您的案例请参阅http://www.codenuclear.com/java-8-date-time-intro 以了解有关即将推出的 Java 功能的更多详细信息

【讨论】:

【参考方案4】:

ZoneId可以通过DateTimeFormatter中的withZone方法指定:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat).withZone("+0530");

【讨论】:

【参考方案5】:

如果没有OFFSET_SECOND,您可以简单地在DateTimeFormatterBuilder 中添加一个默认值:


编辑:要获得系统的默认ZoneOffset,您必须将ZoneRules 应用于当前Instant。结果如下所示:

class DateUtil 
  public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) 
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
    LocalDateTime localDateTime = LocalDateTime.parse(date, formatter);
    ZoneOffset defaultOffset =  ZoneId.systemDefault().getRules().getOffset(localDateTime);
    DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
            .append(formatter)
            .parseDefaulting(ChronoField.OFFSET_SECONDS, defaultOffset.getTotalSeconds())
            .toFormatter();
    return ZonedDateTime.parse(date, dateTimeFormatter);
  

输出:

2017-09-14T15:00+05:30
2017-09-14T15:00+02:00

【讨论】:

在夏令时使用Instant.now() 的偏移量是一个棘手的情况。示例:在我的 JVM 中,默认区域是 America/Sao_Paulo。冬季偏移量为 -03:00,DST 期间(从 10 月开始)偏移量为 -02:00。如果我使用参数"2017-11-14 15:00:00", "yyyy-MM-dd HH:mm:ss" 调用您的方法,它将在 11 月 11 日 15:00 和当前偏移量 (-03:00) 获得一个日期,但正确的 (IMO) 应该是在本地日期有效的偏移量对应于输入(在这种情况下,在 11 月,当它已经是 DST 时,所以偏移量应该是 -02:00)。 然后查看 API 并选择正确的方法:ZoneRules::getOffsetZoneRules::getStandardOffset。 OP 没有那么具体。【参考方案6】:

只是从我的项目中复制此解决方案:

formatter = DateTimeFormatter.ofPattern(dateFormat).withZone(ZONE_UTC);

格式化程序编译完成后,您可以调用withZone(ZoneId) 来创建具有设定时区的新格式化程序。

【讨论】:

这将忽略第一个输入中的偏移量+0530 然后试试 OffsetDateTime,因为 Zoned 需要知道区域规则。 好吧,OP 要求 ZonedDateTime 和默认时区(不是 UTC)。

以上是关于Java 8 Time API - ZonedDateTime - 解析时指定默认 ZoneId的主要内容,如果未能解决你的问题,请参考以下文章

Java 8的Time包常用API

java 使用Java 8 Date and Time API(JSR 310)的示例

java 使用Java 8 Date and Time API(JSR 310)的示例

Java 8 Time API 的 ObjectMapper 配置

java_JDK8中新增的时间API

Java日期时间API系列8-----Jdk8中java.time包中的新的日期时间API类的LocalDate源码分析