2

I have a function that parses an Unix epoch time into the format yyyy-MM-dd'T'HH:mm:ss.SSSXXX like this in order to export it to a file:

    public static final SimpleDateFormat REQIF_DATE_FORMAT_WITH_MILLIS
        = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");

    public static String convertEpochStringToReqifDateString(String epochString) {
        Date timestamp = new Date(Long.parseLong(epochString));
        return REQIF_DATE_FORMAT_WITH_MILLIS.format(timestamp);
    }

Now, I have tests for this export, but while they pass locally, they fail on the server because it's apparently in a different time zone. Specifically, the differences look like this:

LAST-CHANGE="2017-03-13T21:36:44.261+01:00"
LAST-CHANGE="2017-03-13T20:36:44.261Z"

I have already tried running a number of things before the test in order to make sure that the test is always run in the same time zone, such as:

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
System.setProperty("user.timezone", "UTC");

As well as the JUnitPioneer annotation:

@DefaultTimeZone("UTC")

...however, none of them seemed to affect the parsing output at all.

What can I do about this? All I want is some way to ensure that my tests are run in the same time zone regardless of where the machine they are run is standing so I can test the export correctly.

halfer
  • 19,824
  • 17
  • 99
  • 186
Kira Resari
  • 1,718
  • 4
  • 19
  • 50
  • 1
    Is there a good reason to be using `Date`, as opposed to `java.time.Instant` (which has its own `toString()` method, and _always_ prints in Zulu? – Andy Turner Sep 08 '21 at 14:26
  • (BTW, I hope you're just omitting the code to ensure that only one thread accesses the SimpleDateFormat at a time? If not, you need to ensure that, e.g. using a `ThreadLocal` instead. You wouldn't need to do this with an `Instant`) – Andy Turner Sep 08 '21 at 14:28
  • I recommend you don’t use `SimpleDateFormat` and `Date`. Those classes are poorly designed and long outdated, the former in particular notoriously troublesome. Instead use `Instant` from [java.time, the modern Java date and time API](https://docs.oracle.com/javase/tutorial/datetime/). – Ole V.V. Sep 08 '21 at 17:43
  • Don’t ensure that the tests are run in a specific time zone. Write both your code and your tests so they are independent of the JVM’s default time zone. – Ole V.V. Sep 11 '21 at 08:22

2 Answers2

6

java.time

The java.util Date-Time API and their formatting API, SimpleDateFormat are outdated and error-prone. It is recommended to stop using them completely and switch to the modern Date-Time API*.

You can use Instant.ofEpochMilli to convert the epoch milliseconds into Instant and then use the Instant#toString. However, Instant#toString omits the fraction-of-second part if they are zero. Therefore, if you need the value strictly in the pattern, yyyy-MM-dd'T'HH:mm:ss.SSSXXX, you can convert the Instant to OffsetDateTime and format it using a DateTimeFormatter.

Solution using java.time, the modern Date-Time API:

import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class Main {
    public static void main(String[] args) {
        // An example epoch milliseconds
        long millis = 1631113620000L;

        Instant instant = Instant.ofEpochMilli(millis);
        String strDateTime = instant.toString();
        System.out.println(strDateTime);

        // If you need the value strictly in the pattern, yyyy-MM-dd'T'HH:mm:ss.SSSXXX
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSXXX", Locale.ENGLISH);
        OffsetDateTime odt = instant.atOffset(ZoneOffset.UTC);
        strDateTime = odt.format(dtf);
        System.out.println(strDateTime);
    }
}

Output:

2021-09-08T15:07:00Z
2021-09-08T15:07:00.000Z

ONLINE DEMO

Learn more about the modern Date-Time API from Trail: Date Time.


* For any reason, if you have to stick to Java 6 or Java 7, you can use ThreeTen-Backport which backports most of the java.time functionality to Java 6 & 7. If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring and How to use ThreeTenABP in Android Project.

Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
2

A better way to do this would be to use java.time.Instant:

Instant instant = Instant.ofEpochMilli(Long.parseLong(epochString));
return instant.toString();

This will always print in UTC, whatever the JVM's default time zone.


You can also do this by setting the time zone on the SimpleDateFormatter:

REQIF_DATE_FORMAT_WITH_MILLIS.setTimeZone(TimeZone.getTimeZone("UTC"));

Note that you need to take care with a shared SimpleDateFormat because it has mutable state, which is corrupted by access from multiple threads. You can have a separate instance per thread like so:

static final ThreadLocal<SimpleDateFormat> REQIF_DATE_FORMAT_WITH_MILLIS =
    ThreadLocal.withInitial(() -> {
      SimpleDateFormat sdf = new SimpleDateFormat();
      sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
      return sdf;
    });

and then access it with:

return REQIF_DATE_FORMAT_WITH_MILLIS.get().format(timestamp);

But this is getting quite messy, isn't it? Much easier just to use Instant.

Andy Turner
  • 137,514
  • 11
  • 162
  • 243