That’s non-trivial alright. Most of the magic is in the following formatter:
private static final DateTimeFormatter formatter
= new DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE)
.appendPattern("['T'][' ']")
.append(DateTimeFormatter.ISO_LOCAL_TIME)
.appendPattern("[XX]")
.toFormatter();
This formatter accepts
- A date like
2014-05-25
.
- Either a
T
or a space (or both or neither, but I count on exactly one of them being in your string). The square brackets denote optional parts of the format. The single quotes denote literal parts and cause the T
not to be interpreted as a pattern letter.
- A time of day.
ISO_LOCAL_TIME
accepts seconds with and without a decimal fraction.
- An optional UTC offset like
Z
(for zero) or -0600
.
We also need a time zone for the cases where we haven’t got an offset in the string. For example:
private static final ZoneId defaultZone = ZoneId.of("America/Curacao");
What your method still needs to do is distinguish between the cases with and without UTC offset. The form with offset can be parsed directly into a ZonedDateTime
. The one without cannot, so for it we need to parse into a LocalDateTime
and convert.
/**
* @param dateTime : any ISO 8601 format , e.g 1. %Y-%m-%dT%H:%M:%s%z => 2014-05-25T08:20:03.123456Z ,
* e.g 2. %Y-%m-%dT%H:%M:%s => 2014-05-25T08:20:03.123456,
* e.g 3. %Y-%m-%d %H:%M:%s%z => 2014-11-08 15:55:55.123456Z,
* e.g 4. %Y-%m-%d %H:%M:%s => 2014-11-08 15:55:55
* @return Instance of ZonedDateTime if conversion successful
* @throws DateTimeParseException
*/
private static ZonedDateTime parse(String dateTime) {
// Little magic here.
TemporalAccessor parsed = formatter.parseBest(dateTime,
ZonedDateTime::from, LocalDateTime::from);
if (parsed instanceof ZonedDateTime) {
return (ZonedDateTime) parsed;
} else {
return ((LocalDateTime) parsed).atZone(defaultZone);
}
}
Let’s try it out. I have used your four examples plus one that I added between the 1st and the 2nd.
System.out.println(parse("2014-05-25T08:20:03.123456Z"));
System.out.println(parse("2014-05-25T10:20:03.123456+0200"));
System.out.println(parse("2014-05-25T08:20:03.123456"));
System.out.println(parse("2014-11-08 15:55:55.123456Z"));
System.out.println(parse("2014-11-08 15:55:55"));
Output:
2014-05-25T08:20:03.123456Z
2014-05-25T10:20:03.123456+02:00
2014-05-25T08:20:03.123456-04:00[America/Curacao]
2014-11-08T15:55:55.123456Z
2014-11-08T15:55:55-04:00[America/Curacao]
As an aside prefer OffsetDateTime
over ZonedDateTime
for your strings with a UTC offset. The code will be the same except you will need a further conversion .toOffsetDateTime()
after atZone(defaultZone)
in the case where you don’t get an OffsetDateTime
directly from parsing. A ZonedDateTime
is for a date and time with a time zone like Europe/Prague. An OffsetDateTime
is for a date and time with a UTC offset. As I mentioned, Z
is an offset of 0 from UTC.
Edit: Arvind Kumar Avinash’ notes in a comment are useful and important enough to deserve to be in the answer proper: Some notes for the future visitors:
- Replacing
[XX]
with [XX][XXX]
will also cater to the offset like +02:00.
- Date-time parsing/formatting types are Locale-sensitive and therefore, it's always advisable to use it with the applicable locale e.g.
.toFormatter(Locale.ENGLISH)
instead of .toFormatter()
.
- You can use
ZoneId.systemDefault()
if you want the ZoneId
of the JVM to be picked up automatically.
2014-11-08 15:55:55
, is not ISO 8601 compliant. ISO 8601 requires aT
to denote the start of the time part as in your 1st and 2nd example. Same problem with the 3rd example. – TightZ
for UTC, clearly you should get aZonedDateTime
in UTC. When it doesn’t, which time zone do you want? It requires a time zone to construct aZonedDateTime
. – Tight