Java: parse ISO_DATE / ISO_OFFSET_DATE

非 Y 不嫁゛ 提交于 2021-01-05 12:00:28

问题


For a REST web service, I need to return dates (no time) with a time zone.

Apparently there is no such thing as a ZonedDate in Java (only LocalDate and ZonedDateTime), so I'm using ZonedDateTime as a fallback.

When converting those dates to JSON, I use DateTimeFormatter.ISO_OFFSET_DATE to format the date, which works really well:

DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE;
ZonedDateTime dateTime = ZonedDateTime.now();
String formatted = dateTime.format(formatter);

2018-04-19+02:00

However, attempting to parse back such a date with...

ZonedDateTime parsed = ZonedDateTime.parse(formatted, formatter);

... results in an Exception:

java.time.format.DateTimeParseException: Text '2018-04-19+02:00' could not be parsed: Unable to obtain ZonedDateTime from TemporalAccessor: {OffsetSeconds=7200},ISO resolved to 2018-04-19 of type java.time.format.Parsed

I also tried ISO_DATE and ran into the same problem.

How can I parse such a zoned date back?
Or is there any other type (within the Java Time API) I'm supposed to use for zoned dates?


回答1:


The problem is that ZonedDateTime needs all the date and time fields to be built (year, month, day, hour, minute, second, nanosecond), but the formatter ISO_OFFSET_DATE produces a string without the time part.

When parsing it back, there are no time-related fields (hours, minutes, seconds) and you get a DateTimeParseException.

One alternative to parse it is to use a DateTimeFormatterBuilder and define default values for the time fields. As you used atStartOfDay in your answer, I'm assuming you want midnight, so you can do the following:

DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    // date and offset
    .append(DateTimeFormatter.ISO_OFFSET_DATE)
    // default values for hour and minute
    .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
    .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
    .toFormatter();
ZonedDateTime parsed = ZonedDateTime.parse("2018-04-19+02:00", fmt); // 2018-04-19T00:00+02:00

Your solution also works fine, but the only problem is that you're parsing the input twice (each call to formatter.parse will parse the input again). A better alternative is to use the parse method without a temporal query (parse only once), and then use the parsed object to get the information you need.

DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE;
// parse input
TemporalAccessor parsed = formatter.parse("2018-04-19+02:00");

// get data from the parsed object
LocalDate date = LocalDate.from(parsed);
ZoneId zone = ZoneId.from(parsed);
ZonedDateTime restored = date.atStartOfDay(zone); // 2018-04-19T00:00+02:00

With this solution, the input is parsed only once.




回答2:


tl;dr

Use a time zone (continent/region) rather than a mere offset-from-UTC (hours-minutes-seconds). For any particular zone, the offset is likely to change over time.

Combine the two to determine a moment.

LocalDate.parse(
  "2018-04-19"
)
.atStartOfDay( 
    ZoneId.of( "Europe/Zurich" ) 
) // Returns a `ZonedDateTime` object.

2018-04-19T00:00+02:00[Europe/Zurich]

From your REST service, either:

  • Return the date and zone separately (either with a delimiter or as XML/JSON), or,
  • Return the start of day as that is likely the intended outcome of a date with a time zone.

Separate your text inputs

The solution in the Answer by Walser is effectively treating the string input as a pair of string inputs. First the date-only part is extracted and parsed. Second, the offset-from-UTC part is extracted and parsed. So, the input is parsed twice, each time ignoring the opposite half of the string.

I suggest you make this practice explicit. Track the date as one piece of text, track the offset (or, better, a time zone) as another piece of text. As the code in that other Answer demonstrates, there is no real meaning to a date with zone until you take the next step of determining an actual moment such as the start of day.

String inputDate = "2018-04-19" ;
LocalDate ld = LocalDate.parse( inputDate ) ;

String inputOffset = "+02:00" ;
ZoneOffset offset = ZoneOffset.of( inputOffset) ;

OffsetTime ot = OffsetTime.of( LocalTime.MIN , offset ) ; 
OffsetDateTime odt = ld.atTime( ot ) ;  // Use `OffsetDateTime` & `ZoneOffset` when given a offset-from-UTC. Use `ZonedDateTime` and `ZoneId` when given a time zone rather than a mere offset.

odt.toString(): 2018-04-19T00:00+02:00

As you can see, the code is simple, and your intent is obvious.

And no need to bother with any DateTimeFormatter object nor formatting patterns. Those inputs conform with ISO 8601 standard formats. The java.time classes use those standard formats by default when parsing/generating strings.

Offset versus Zone

As for applying the date and offset to get a moment, you are conflating a offset-from-UTC with a time zone. An offset is simply a number of hours, minutes, and seconds. No more, no less. In contrast, a time zone is a history of the past, present, and future changes in offset used by the people of a particular region.

In other words, the +02:00 happens to be used by many time zones on many dates. But in a particular zone, such as Europe/Zurich, other offsets may be used on other dates. For example, adopting the silliness of Daylight Saving Time (DST) means a zone will be spending half the year with one offset and the other half with a different offset.

Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

ZoneId z = ZoneId.of( "Europe/Zurich" ) ;  
ZonedDateTime zdt = ld.atStartOfDay( z ) ;

zdt.toString(): 2018-04-19T00:00+02:00[Europe/Zurich]

So I suggest you track two strings of input:

  • Date-only (LocalDate): YYYY-MM-DD such as 2018-04-19
  • Proper time zone name (ZoneId): continent/region such as Europe/Zurich

Combine.

ZonedDateTime zdt = 
    LocalDate.parse( inputDate )
             .atStartOfDay( ZoneId.of( inputZone ) ) 
;

Note: The ZonedDateTime::toString method generates a String in a format that wisely extends the standard ISO 8601 format by appending the name of the time zone in square brackets. This rectifies a huge oversight made by the otherwise well-designed standard. But you can only return such a string by your REST service if you know your clients can consume it.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

  • Java SE 8, Java SE 9, Java SE 10, and later
    • Built-in.
    • Part of the standard Java API with a bundled implementation.
    • Java 9 adds some minor features and fixes.
  • Java SE 6 and Java SE 7
    • Much of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
  • Android
    • Later versions of Android bundle implementations of the java.time classes.
    • For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.




回答3:


I found the solution (using TemporalQueries):
parse the date and zone separately, and restore the zoned date using that information:

LocalDate date = formatter.parse(formatted, TemporalQueries.localDate());
ZoneId zone = formatter.parse(formatted, TemporalQueries.zone());
ZonedDateTime restored = date.atStartOfDay(zone);


来源:https://stackoverflow.com/questions/49922244/java-parse-iso-date-iso-offset-date

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!