关于时区:Java 8 Time API – ZonedDateTime – 在解析时指定默认的ZoneId

Java 8 Time API - ZonedDateTime - specify default ZoneId when parsing

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

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

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

这里的问题是使用固定的时区。 我将格式指定为参数。 日期及其格式都是String参数。 更通用。

代码和输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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"));
    }
}

产量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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


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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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默认时区,但是即使在运行时也可以在不事先通知的情况下进行更改,因此最好始终明确指出您正在使用的是哪一个。

API使用IANA时区名称(始终采用Region/City格式,如America/Sao_PauloEurope/Berlin)。
避免使用3个字母的缩写(如CSTPST),因为它们不明确且不标准。

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

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

另一种方法是使用parseBest()方法,尝试创建一个合适的日期对象(使用TemporalQuery的列表),直到它创建所需的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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等),使用from方法与parseBest一起使用 - 您还可以创建自己的自定义TemporalQuery如果你愿意,但我认为内置方法足以满足这种情况)。

夏令时

使用atZone()方法将LocalDateTime转换为ZonedDateTime时,有一些关于夏令时(DST)的棘手案例。

我将使用我所居住的时区(America/Sao_Paulo)作为示例,但这可能发生在使用DST的任何时区。

在S?o Paulo,DST于2016年10月16日开始:在午夜,时钟从午夜向上移动1小时到凌晨1点(偏移从-03:00变为-02:00)。因此,在这个时区中,00:00到00:59之间的所有当地时间都不存在(你也可以认为时钟从23:59:59.999999999直接变为01:00)。如果我在此间隔中创建本地日期,则会将其调整为下一个有效时刻:

1
2
3
4
5
6
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日午夜时钟,时钟从18点的午夜到晚上23点向后移动1小时(偏移量从-02:00变为-03:00)。所以从23:00到23:59的所有当地时间都存在两次(在两个偏移中:-03:00-02:00),你必须决定你想要哪一个。
默认情况下,它使用DST结束前的偏移量,但您可以使用withLaterOffsetAtOverlap()方法在DST结束后获取偏移量:

1
2
3
4
5
6
7
8
9
10
// 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]

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


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

为了满足给定的问题,你必须把try catch放在它将考虑默认时区的任何异常的情况下。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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"));
    }
}

有关即将推出的Java功能的更多详细信息,请参考http://www.codenuclear.com/java-8-date-time-intro


java.time库中没有默认设置,这是一件好事 - 你所看到的就是你得到的东西,句号。

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

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

但是,如果你真的必须,这是另一种方法来做你想要的(一种不使用异常来控制流的替代解决方案):

1
2
3
4
5
TemporalAccessor parsed = f.parse(string);
if (parsed.query(TemporalQueries.zone()) == null) {
  parsed = f.withZone(ZoneId.systemDefault()).parse(string);
}
return ZonedDateTime.from(parsed);

这里我们使用中间解析结果来确定字符串是否包含区域信息,如果没有,我们再次解析(使用相同的字符串,但不同的打印机解析器),这次它将包含一个区域。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
  }
}

如果没有OFFSET_SECOND,您只需在DateTimeFormatterBuilder中添加默认值:

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

1
2
3
4
5
6
7
8
9
10
11
12
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);
  }
}

输出:

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


可以使用DateTimeFormatter中的withZone方法指定ZoneId

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

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

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

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