How to convert timestamp string to epoch time?

User7723337 picture User7723337 · Aug 18, 2017 · Viewed 9.3k times · Source

I have time stamp in format 2017-18-08 11:45:30.345.
I want to convert it to epoch time, so I am doing below:

String timeDateStr = "2017-18-08 11:45:30.345"; 
DateTimeFormatter dtf  = DateTimeFormatter.ofPattern("yyyy-dd-MM HH:mm:ss.SSS");
ZonedDateTime     zdt  = ZonedDateTime.parse(timeDateStr, dtf);        
System.out.println(zdt.toInstant().toEpochMilli());

I am getting below error:

java.time.format.DateTimeParseException: Text '2017-18-08 11:45:30.345' could not be parsed: Unable to obtain ZonedDateTime from TemporalAccessor

I also tried different formats but still getting errors.

Answer

user7605325 picture user7605325 · Aug 18, 2017

Note: originally the question had the input 2017-18-08 12:60:30.345 (with 60 in the minutes field), then it was edited (the time changed from 12:60 to 11:45), but I decided to keep this answer discussing about the original input (12:60), as it also works for the edited version (11:45).


ZonedDateTime needs a timezone or offset, but the input String doesn't have it (it has only date and time).

There are also another details in the input:

  • the minute value is 60, which is not accepted: the valid values are from 0 to 59 (actually there's a way to accept this, see "Lenient parsing" below)
  • the hh is the clock-hour-of-am-pm field, so it also needs the AM/PM designator to be fully resolved. As you don't have it, you should use the HH pattern instead

So the pattern must be yyyy-dd-MM HH:mm:ss.SSS, the input can't have 60 as the minutes value (unless you use lenient parsing, which I'll explain below) and you can't direclty parse it to a ZonedDateTime because it doesn't have a timezone/offset designator.

One alternative is to parse it to a LocalDateTime and then define in which timezone/offset this date is. In the example below, I'm assuming it's in UTC:

// change 60 minutes to 59 (otherwise it doesn't work)
String timeDateStr = "2017-18-08 12:59:30.345";
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-dd-MM HH:mm:ss.SSS");
// parse to LocalDateTime
LocalDateTime dt = LocalDateTime.parse(timeDateStr, dtf);

// assume the LocalDateTime is in UTC
Instant instant = dt.toInstant(ZoneOffset.UTC);
System.out.println(instant.toEpochMilli());

This will output:

1503061170345

Which is the equivalent of 2017-18-08 12:59:30.345 in UTC.

If you want the date in another timezone, you can use the ZoneId class:

// get the LocalDateTime in some timezone
ZonedDateTime z = dt.atZone(ZoneId.of("Europe/London"));
System.out.println(z.toInstant().toEpochMilli());

The output is:

1503057570345

Note that the result is different, because the same local date/time represents a different Instant in each timezone (in each part of the world, the local date/time 2017-18-08 12:59:30.345 happened in a different instant).

Also note that API uses IANA timezones names (always in the format Region/City, like America/Sao_Paulo or Europe/Berlin). Avoid using the 3-letter abbreviations (like CST or PST) because they are ambiguous and not standard.

You can get a list of available timezones (and choose the one that fits best your system) by calling ZoneId.getAvailableZoneIds().

You can also use the system's default timezone with ZoneId.systemDefault(), but this can be changed without notice, even at runtime, so it's better to explicity use a specific one.


There's also the option to convert the LocalDateTime to an offset (like -05:00 or +03:00):

// get the LocalDateTime in +03:00 offset
System.out.println(dt.toInstant(ZoneOffset.ofHours(3)).toEpochMilli());

The output will be equivalent to the local date/time in the offset +03:00 (3 hours ahead of UTC):

1503050370345


Lenient parsing

As @MenoHochschild reminded me in the comments, you can use lenient parsing to accept 60 in the minutes field (using the java.time.format.ResolverStyle class):

String timeDateStr = "2017-18-08 12:60:30.345";
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-dd-MM HH:mm:ss.SSS")
    // use lenient parsing
    .withResolverStyle(ResolverStyle.LENIENT);
// parse to LocalDateTime
LocalDateTime dt = LocalDateTime.parse(timeDateStr, dtf);

In this case, 60 minutes are adjusted to the next hour, and the LocalDateTime will be:

2017-08-18T13:00:30.345


Daylight Saving Time

If you decide to use UTC or a fixed offset (using ZoneOffset class), you can ignore this section.

But if you decide to use a timezone (with ZoneId class), you must also take care of DST (Daylight Saving Time) issues. I'm gonna use the timezone I live in as example (America/Sao_Paulo).

In São Paulo, DST starts at October 15th 2017: at midnight, clocks shift 1 hour forward from midnight to 1 AM. So all local times between 00:00 and 00:59 don't exist in this timezone. If I create a local date in this interval, it's adjusted to the next valid moment:

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

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

When DST ends: in February 18th 2018 at midnight, clocks shift back 1 hour, from midnight to 23 PM of 17th. So all local times from 23:00 to 23:59 exist twice (in DST and in non-DST), and you must decide which one you want:

// February 18th 2018 at midnight, DST ends in Sao Paulo
// local times from 23:00 to 23:59 at 17th exist twice
LocalDateTime d = LocalDateTime.of(2018, 2, 17, 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]

Note that the dates before and after DST ends have different offsets (-02:00 and -03:00). This affects the value of epochMilli.

You must check when DST starts and ends in the timezone you choose and check the adjustments accordingly.